diff --git a/Handlers/ActivityIndicatorHandler.cs b/Handlers/ActivityIndicatorHandler.cs index 4277835..a61c96c 100644 --- a/Handlers/ActivityIndicatorHandler.cs +++ b/Handlers/ActivityIndicatorHandler.cs @@ -1,62 +1,64 @@ -using Microsoft.Maui.Graphics; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using Microsoft.Maui.Handlers; +using Microsoft.Maui.Graphics; +using SkiaSharp; namespace Microsoft.Maui.Platform.Linux.Handlers; -public class ActivityIndicatorHandler : ViewHandler +/// +/// Handler for ActivityIndicator on Linux using Skia rendering. +/// Maps IActivityIndicator interface to SkiaActivityIndicator platform view. +/// +public partial class ActivityIndicatorHandler : ViewHandler { - public static IPropertyMapper Mapper = (IPropertyMapper)(object)new PropertyMapper((IPropertyMapper[])(object)new IPropertyMapper[1] { (IPropertyMapper)ViewHandler.ViewMapper }) - { - ["IsRunning"] = MapIsRunning, - ["Color"] = MapColor, - ["Background"] = MapBackground - }; + public static IPropertyMapper Mapper = new PropertyMapper(ViewHandler.ViewMapper) + { + [nameof(IActivityIndicator.IsRunning)] = MapIsRunning, + [nameof(IActivityIndicator.Color)] = MapColor, + [nameof(IView.Background)] = MapBackground, + }; - public static CommandMapper CommandMapper = new CommandMapper((CommandMapper)(object)ViewHandler.ViewCommandMapper); + public static CommandMapper CommandMapper = new(ViewHandler.ViewCommandMapper) + { + }; - public ActivityIndicatorHandler() - : base((IPropertyMapper)(object)Mapper, (CommandMapper)(object)CommandMapper) - { - } + public ActivityIndicatorHandler() : base(Mapper, CommandMapper) + { + } - public ActivityIndicatorHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null) - : base((IPropertyMapper)(((object)mapper) ?? ((object)Mapper)), (CommandMapper)(((object)commandMapper) ?? ((object)CommandMapper))) - { - } + public ActivityIndicatorHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null) + : base(mapper ?? Mapper, commandMapper ?? CommandMapper) + { + } - protected override SkiaActivityIndicator CreatePlatformView() - { - return new SkiaActivityIndicator(); - } + protected override SkiaActivityIndicator CreatePlatformView() + { + return new SkiaActivityIndicator(); + } - public static void MapIsRunning(ActivityIndicatorHandler handler, IActivityIndicator activityIndicator) - { - if (((ViewHandler)(object)handler).PlatformView != null) - { - ((ViewHandler)(object)handler).PlatformView.IsRunning = activityIndicator.IsRunning; - } - } + public static void MapIsRunning(ActivityIndicatorHandler handler, IActivityIndicator activityIndicator) + { + if (handler.PlatformView is null) return; + handler.PlatformView.IsRunning = activityIndicator.IsRunning; + } - public static void MapColor(ActivityIndicatorHandler handler, IActivityIndicator activityIndicator) - { - //IL_001d: Unknown result type (might be due to invalid IL or missing references) - if (((ViewHandler)(object)handler).PlatformView != null && activityIndicator.Color != null) - { - ((ViewHandler)(object)handler).PlatformView.Color = activityIndicator.Color.ToSKColor(); - } - } + public static void MapColor(ActivityIndicatorHandler handler, IActivityIndicator activityIndicator) + { + if (handler.PlatformView is null) return; - public static void MapBackground(ActivityIndicatorHandler handler, IActivityIndicator activityIndicator) - { - //IL_002c: Unknown result type (might be due to invalid IL or missing references) - if (((ViewHandler)(object)handler).PlatformView != null) - { - Paint background = ((IView)activityIndicator).Background; - SolidPaint val = (SolidPaint)(object)((background is SolidPaint) ? background : null); - if (val != null && val.Color != null) - { - ((ViewHandler)(object)handler).PlatformView.BackgroundColor = val.Color.ToSKColor(); - } - } - } + if (activityIndicator.Color is not null) + handler.PlatformView.Color = activityIndicator.Color.ToSKColor(); + } + + public static void MapBackground(ActivityIndicatorHandler handler, IActivityIndicator activityIndicator) + { + if (handler.PlatformView is null) return; + + if (activityIndicator.Background is SolidPaint solidPaint && solidPaint.Color is not null) + { + handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor(); + } + } } diff --git a/Handlers/ApplicationHandler.cs b/Handlers/ApplicationHandler.cs index c78c3d4..6a34b59 100644 --- a/Handlers/ApplicationHandler.cs +++ b/Handlers/ApplicationHandler.cs @@ -1,59 +1,137 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using Microsoft.Maui.Handlers; +using Microsoft.Maui.Controls; +using Microsoft.Maui.Platform.Linux.Hosting; namespace Microsoft.Maui.Platform.Linux.Handlers; -public class ApplicationHandler : ElementHandler +/// +/// Handler for MAUI Application on Linux. +/// Bridges the MAUI Application lifecycle with LinuxApplication. +/// +public partial class ApplicationHandler : ElementHandler { - public static IPropertyMapper Mapper = (IPropertyMapper)(object)new PropertyMapper((IPropertyMapper[])(object)new IPropertyMapper[1] { (IPropertyMapper)ElementHandler.ElementMapper }); + public static IPropertyMapper Mapper = + new PropertyMapper(ElementHandler.ElementMapper) + { + }; - public static CommandMapper CommandMapper = new CommandMapper((CommandMapper)(object)ElementHandler.ElementCommandMapper) - { - ["OpenWindow"] = MapOpenWindow, - ["CloseWindow"] = MapCloseWindow - }; + public static CommandMapper CommandMapper = + new(ElementHandler.ElementCommandMapper) + { + [nameof(IApplication.OpenWindow)] = MapOpenWindow, + [nameof(IApplication.CloseWindow)] = MapCloseWindow, + }; - public ApplicationHandler() - : base((IPropertyMapper)(object)Mapper, (CommandMapper)(object)CommandMapper) - { - } + public ApplicationHandler() : base(Mapper, CommandMapper) + { + } - public ApplicationHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null) - : base((IPropertyMapper)(((object)mapper) ?? ((object)Mapper)), (CommandMapper)(((object)commandMapper) ?? ((object)CommandMapper))) - { - } + public ApplicationHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null) + : base(mapper ?? Mapper, commandMapper ?? CommandMapper) + { + } - protected override LinuxApplicationContext CreatePlatformElement() - { - return new LinuxApplicationContext(); - } + protected override LinuxApplicationContext CreatePlatformElement() + { + return new LinuxApplicationContext(); + } - protected override void ConnectHandler(LinuxApplicationContext platformView) - { - base.ConnectHandler(platformView); - platformView.Application = base.VirtualView; - } + protected override void ConnectHandler(LinuxApplicationContext platformView) + { + base.ConnectHandler(platformView); + platformView.Application = VirtualView; + } - protected override void DisconnectHandler(LinuxApplicationContext platformView) - { - platformView.Application = null; - base.DisconnectHandler(platformView); - } + protected override void DisconnectHandler(LinuxApplicationContext platformView) + { + platformView.Application = null; + base.DisconnectHandler(platformView); + } - public static void MapOpenWindow(ApplicationHandler handler, IApplication application, object? args) - { - IWindow val = (IWindow)((args is IWindow) ? args : null); - if (val != null) - { - ((ElementHandler)(object)handler).PlatformView?.OpenWindow(val); - } - } + public static void MapOpenWindow(ApplicationHandler handler, IApplication application, object? args) + { + if (args is IWindow window) + { + handler.PlatformView?.OpenWindow(window); + } + } - public static void MapCloseWindow(ApplicationHandler handler, IApplication application, object? args) - { - IWindow val = (IWindow)((args is IWindow) ? args : null); - if (val != null) - { - ((ElementHandler)(object)handler).PlatformView?.CloseWindow(val); - } - } + public static void MapCloseWindow(ApplicationHandler handler, IApplication application, object? args) + { + if (args is IWindow window) + { + handler.PlatformView?.CloseWindow(window); + } + } +} + +/// +/// Platform context for the MAUI Application on Linux. +/// Manages windows and the application lifecycle. +/// +public class LinuxApplicationContext +{ + private readonly List _windows = new(); + private IApplication? _application; + + /// + /// Gets or sets the MAUI Application. + /// + public IApplication? Application + { + get => _application; + set + { + _application = value; + if (_application != null) + { + // Initialize windows from the application + foreach (var window in _application.Windows) + { + if (!_windows.Contains(window)) + { + _windows.Add(window); + } + } + } + } + } + + /// + /// Gets the list of open windows. + /// + public IReadOnlyList Windows => _windows; + + /// + /// Opens a window and creates its handler. + /// + public void OpenWindow(IWindow window) + { + if (!_windows.Contains(window)) + { + _windows.Add(window); + } + } + + /// + /// Closes a window and cleans up its handler. + /// + public void CloseWindow(IWindow window) + { + _windows.Remove(window); + + if (_windows.Count == 0) + { + // Last window closed, stop the application + LinuxApplication.Current?.MainWindow?.Stop(); + } + } + + /// + /// Gets the main window of the application. + /// + public IWindow? MainWindow => _windows.Count > 0 ? _windows[0] : null; } diff --git a/Handlers/BorderHandler.cs b/Handlers/BorderHandler.cs index ed366b8..a0392a9 100644 --- a/Handlers/BorderHandler.cs +++ b/Handlers/BorderHandler.cs @@ -1,186 +1,158 @@ -using System; -using Microsoft.Maui.Controls; -using Microsoft.Maui.Controls.Shapes; -using Microsoft.Maui.Graphics; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using Microsoft.Maui.Handlers; -using Microsoft.Maui.Platform.Linux.Hosting; +using Microsoft.Maui.Graphics; +using Microsoft.Maui.Controls; +using Microsoft.Maui.Platform; +using SkiaSharp; namespace Microsoft.Maui.Platform.Linux.Handlers; -public class BorderHandler : ViewHandler +/// +/// Handler for Border on Linux using Skia rendering. +/// +public partial class BorderHandler : ViewHandler { - public static IPropertyMapper Mapper = (IPropertyMapper)(object)new PropertyMapper((IPropertyMapper[])(object)new IPropertyMapper[1] { (IPropertyMapper)ViewHandler.ViewMapper }) - { - ["Content"] = MapContent, - ["Stroke"] = MapStroke, - ["StrokeThickness"] = MapStrokeThickness, - ["StrokeShape"] = MapStrokeShape, - ["Background"] = MapBackground, - ["BackgroundColor"] = MapBackgroundColor, - ["Padding"] = MapPadding - }; + public static IPropertyMapper Mapper = + new PropertyMapper(ViewHandler.ViewMapper) + { + [nameof(IBorderView.Content)] = MapContent, + [nameof(IBorderStroke.Stroke)] = MapStroke, + [nameof(IBorderStroke.StrokeThickness)] = MapStrokeThickness, + ["StrokeShape"] = MapStrokeShape, // StrokeShape is on Border, not IBorderStroke + [nameof(IView.Background)] = MapBackground, + ["BackgroundColor"] = MapBackgroundColor, + [nameof(IPadding.Padding)] = MapPadding, + }; - public static CommandMapper CommandMapper = new CommandMapper((CommandMapper)(object)ViewHandler.ViewCommandMapper); + public static CommandMapper CommandMapper = + new(ViewHandler.ViewCommandMapper) + { + }; - public BorderHandler() - : base((IPropertyMapper)(object)Mapper, (CommandMapper)(object)CommandMapper) - { - } + public BorderHandler() : base(Mapper, CommandMapper) + { + } - public BorderHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null) - : base((IPropertyMapper)(((object)mapper) ?? ((object)Mapper)), (CommandMapper)(((object)commandMapper) ?? ((object)CommandMapper))) - { - } + public BorderHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null) + : base(mapper ?? Mapper, commandMapper ?? CommandMapper) + { + } - protected override SkiaBorder CreatePlatformView() - { - return new SkiaBorder(); - } + protected override SkiaBorder CreatePlatformView() + { + return new SkiaBorder(); + } - protected override void ConnectHandler(SkiaBorder platformView) - { - base.ConnectHandler(platformView); - IBorderView virtualView = base.VirtualView; - View val = (View)(object)((virtualView is View) ? virtualView : null); - if (val != null) - { - platformView.MauiView = val; - } - platformView.Tapped += OnPlatformViewTapped; - } + protected override void ConnectHandler(SkiaBorder platformView) + { + base.ConnectHandler(platformView); + } - protected override void DisconnectHandler(SkiaBorder platformView) - { - platformView.Tapped -= OnPlatformViewTapped; - platformView.MauiView = null; - base.DisconnectHandler(platformView); - } + protected override void DisconnectHandler(SkiaBorder platformView) + { + base.DisconnectHandler(platformView); + } - private void OnPlatformViewTapped(object? sender, EventArgs e) - { - IBorderView virtualView = base.VirtualView; - View val = (View)(object)((virtualView is View) ? virtualView : null); - if (val != null) - { - GestureManager.ProcessTap(val, 0.0, 0.0); - } - } + public static void MapContent(BorderHandler handler, IBorderView border) + { + if (handler.PlatformView is null || handler.MauiContext is null) return; - public static void MapContent(BorderHandler handler, IBorderView border) - { - if (((ViewHandler)(object)handler).PlatformView == null || ((ElementHandler)handler).MauiContext == null) - { - return; - } - ((ViewHandler)(object)handler).PlatformView.ClearChildren(); - IView presentedContent = ((IContentView)border).PresentedContent; - if (presentedContent != null) - { - if (presentedContent.Handler == null) - { - Console.WriteLine("[BorderHandler] Creating handler for content: " + ((object)presentedContent).GetType().Name); - presentedContent.Handler = presentedContent.ToViewHandler(((ElementHandler)handler).MauiContext); - } - IViewHandler handler2 = presentedContent.Handler; - if (((handler2 != null) ? ((IElementHandler)handler2).PlatformView : null) is SkiaView skiaView) - { - Console.WriteLine("[BorderHandler] Adding content: " + ((object)skiaView).GetType().Name); - ((ViewHandler)(object)handler).PlatformView.AddChild(skiaView); - } - } - } + handler.PlatformView.ClearChildren(); - public static void MapStroke(BorderHandler handler, IBorderView border) - { - //IL_002c: Unknown result type (might be due to invalid IL or missing references) - if (((ViewHandler)(object)handler).PlatformView != null) - { - Paint stroke = ((IStroke)border).Stroke; - SolidPaint val = (SolidPaint)(object)((stroke is SolidPaint) ? stroke : null); - if (val != null && val.Color != null) - { - ((ViewHandler)(object)handler).PlatformView.Stroke = val.Color.ToSKColor(); - } - } - } + var content = border.PresentedContent; + if (content != null) + { + // Create handler for content if it doesn't exist + if (content.Handler == null) + { + Console.WriteLine($"[BorderHandler] Creating handler for content: {content.GetType().Name}"); + content.Handler = content.ToHandler(handler.MauiContext); + } - public static void MapStrokeThickness(BorderHandler handler, IBorderView border) - { - if (((ViewHandler)(object)handler).PlatformView != null) - { - ((ViewHandler)(object)handler).PlatformView.StrokeThickness = (float)((IStroke)border).StrokeThickness; - } - } + if (content.Handler?.PlatformView is SkiaView skiaContent) + { + Console.WriteLine($"[BorderHandler] Adding content: {skiaContent.GetType().Name}"); + handler.PlatformView.AddChild(skiaContent); + } + } + } - public static void MapBackground(BorderHandler handler, IBorderView border) - { - //IL_002c: Unknown result type (might be due to invalid IL or missing references) - if (((ViewHandler)(object)handler).PlatformView != null) - { - Paint background = ((IView)border).Background; - SolidPaint val = (SolidPaint)(object)((background is SolidPaint) ? background : null); - if (val != null && val.Color != null) - { - ((ViewHandler)(object)handler).PlatformView.BackgroundColor = val.Color.ToSKColor(); - } - } - } + public static void MapStroke(BorderHandler handler, IBorderView border) + { + if (handler.PlatformView is null) return; - public static void MapBackgroundColor(BorderHandler handler, IBorderView border) - { - //IL_0027: Unknown result type (might be due to invalid IL or missing references) - if (((ViewHandler)(object)handler).PlatformView != null) - { - VisualElement val = (VisualElement)(object)((border is VisualElement) ? border : null); - if (val != null && val.BackgroundColor != null) - { - ((ViewHandler)(object)handler).PlatformView.BackgroundColor = val.BackgroundColor.ToSKColor(); - ((ViewHandler)(object)handler).PlatformView.Invalidate(); - } - } - } + if (border.Stroke is SolidPaint solidPaint && solidPaint.Color is not null) + { + handler.PlatformView.Stroke = solidPaint.Color.ToSKColor(); + } + } - public static void MapPadding(BorderHandler handler, IBorderView border) - { - //IL_000a: Unknown result type (might be due to invalid IL or missing references) - //IL_000f: Unknown result type (might be due to invalid IL or missing references) - if (((ViewHandler)(object)handler).PlatformView != null) - { - Thickness padding = ((IPadding)border).Padding; - ((ViewHandler)(object)handler).PlatformView.PaddingLeft = (float)((Thickness)(ref padding)).Left; - ((ViewHandler)(object)handler).PlatformView.PaddingTop = (float)((Thickness)(ref padding)).Top; - ((ViewHandler)(object)handler).PlatformView.PaddingRight = (float)((Thickness)(ref padding)).Right; - ((ViewHandler)(object)handler).PlatformView.PaddingBottom = (float)((Thickness)(ref padding)).Bottom; - } - } + public static void MapStrokeThickness(BorderHandler handler, IBorderView border) + { + if (handler.PlatformView is null) return; + handler.PlatformView.StrokeThickness = (float)border.StrokeThickness; + } - public static void MapStrokeShape(BorderHandler handler, IBorderView border) - { - //IL_0026: Unknown result type (might be due to invalid IL or missing references) - //IL_002b: Unknown result type (might be due to invalid IL or missing references) - if (((ViewHandler)(object)handler).PlatformView == null) - { - return; - } - Border val = (Border)(object)((border is Border) ? border : null); - if (val != null) - { - IShape strokeShape = val.StrokeShape; - RoundRectangle val2 = (RoundRectangle)(object)((strokeShape is RoundRectangle) ? strokeShape : null); - if (val2 != null) - { - CornerRadius cornerRadius = val2.CornerRadius; - ((ViewHandler)(object)handler).PlatformView.CornerRadius = (float)((CornerRadius)(ref cornerRadius)).TopLeft; - } - else if (strokeShape is Rectangle) - { - ((ViewHandler)(object)handler).PlatformView.CornerRadius = 0f; - } - else if (strokeShape is Ellipse) - { - ((ViewHandler)(object)handler).PlatformView.CornerRadius = float.MaxValue; - } - ((ViewHandler)(object)handler).PlatformView.Invalidate(); - } - } + public static void MapBackground(BorderHandler handler, IBorderView border) + { + if (handler.PlatformView is null) return; + + if (border.Background is SolidPaint solidPaint && solidPaint.Color is not null) + { + handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor(); + } + } + + public static void MapBackgroundColor(BorderHandler handler, IBorderView border) + { + if (handler.PlatformView is null) return; + + if (border is VisualElement ve && ve.BackgroundColor != null) + { + handler.PlatformView.BackgroundColor = ve.BackgroundColor.ToSKColor(); + handler.PlatformView.Invalidate(); + } + } + + public static void MapPadding(BorderHandler handler, IBorderView border) + { + if (handler.PlatformView is null) return; + + var padding = border.Padding; + handler.PlatformView.PaddingLeft = (float)padding.Left; + handler.PlatformView.PaddingTop = (float)padding.Top; + handler.PlatformView.PaddingRight = (float)padding.Right; + handler.PlatformView.PaddingBottom = (float)padding.Bottom; + } + + public static void MapStrokeShape(BorderHandler handler, IBorderView border) + { + if (handler.PlatformView is null) return; + + // StrokeShape is on the Border control class, not IBorderView interface + if (border is not Border borderControl) return; + + var shape = borderControl.StrokeShape; + if (shape is Microsoft.Maui.Controls.Shapes.RoundRectangle roundRect) + { + // RoundRectangle can have different corner radii, but we use a uniform one + // Take the top-left corner as the uniform radius + var cornerRadius = roundRect.CornerRadius; + handler.PlatformView.CornerRadius = (float)cornerRadius.TopLeft; + } + else if (shape is Microsoft.Maui.Controls.Shapes.Rectangle) + { + handler.PlatformView.CornerRadius = 0; + } + else if (shape is Microsoft.Maui.Controls.Shapes.Ellipse) + { + // For ellipse, use half the min dimension as corner radius + // This will be applied during rendering when bounds are known + handler.PlatformView.CornerRadius = float.MaxValue; // Marker for "fully rounded" + } + + handler.PlatformView.Invalidate(); + } } diff --git a/Handlers/BoxViewHandler.cs b/Handlers/BoxViewHandler.cs index b04e0bc..6b9ddd5 100644 --- a/Handlers/BoxViewHandler.cs +++ b/Handlers/BoxViewHandler.cs @@ -1,66 +1,67 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using Microsoft.Maui.Controls; using Microsoft.Maui.Handlers; using SkiaSharp; namespace Microsoft.Maui.Platform.Linux.Handlers; -public class BoxViewHandler : ViewHandler +/// +/// Handler for BoxView on Linux. +/// +public partial class BoxViewHandler : ViewHandler { - public static IPropertyMapper Mapper = (IPropertyMapper)(object)new PropertyMapper((IPropertyMapper[])(object)new IPropertyMapper[1] { (IPropertyMapper)ViewHandler.ViewMapper }) - { - ["Color"] = MapColor, - ["CornerRadius"] = MapCornerRadius, - ["Background"] = MapBackground, - ["BackgroundColor"] = MapBackgroundColor - }; + public static IPropertyMapper Mapper = + new PropertyMapper(ViewMapper) + { + [nameof(BoxView.Color)] = MapColor, + [nameof(BoxView.CornerRadius)] = MapCornerRadius, + [nameof(IView.Background)] = MapBackground, + ["BackgroundColor"] = MapBackgroundColor, + }; - public BoxViewHandler() - : base((IPropertyMapper)(object)Mapper, (CommandMapper)null) - { - } + public BoxViewHandler() : base(Mapper) + { + } - protected override SkiaBoxView CreatePlatformView() - { - return new SkiaBoxView(); - } + protected override SkiaBoxView CreatePlatformView() + { + return new SkiaBoxView(); + } - public static void MapColor(BoxViewHandler handler, BoxView boxView) - { - //IL_0056: Unknown result type (might be due to invalid IL or missing references) - if (boxView.Color != null) - { - ((ViewHandler)(object)handler).PlatformView.Color = new SKColor((byte)(boxView.Color.Red * 255f), (byte)(boxView.Color.Green * 255f), (byte)(boxView.Color.Blue * 255f), (byte)(boxView.Color.Alpha * 255f)); - } - } + public static void MapColor(BoxViewHandler handler, BoxView boxView) + { + if (boxView.Color != null) + { + handler.PlatformView.Color = new SKColor( + (byte)(boxView.Color.Red * 255), + (byte)(boxView.Color.Green * 255), + (byte)(boxView.Color.Blue * 255), + (byte)(boxView.Color.Alpha * 255)); + } + } - public static void MapCornerRadius(BoxViewHandler handler, BoxView boxView) - { - //IL_0007: Unknown result type (might be due to invalid IL or missing references) - //IL_000c: Unknown result type (might be due to invalid IL or missing references) - SkiaBoxView platformView = ((ViewHandler)(object)handler).PlatformView; - CornerRadius cornerRadius = boxView.CornerRadius; - platformView.CornerRadius = (float)((CornerRadius)(ref cornerRadius)).TopLeft; - } + public static void MapCornerRadius(BoxViewHandler handler, BoxView boxView) + { + handler.PlatformView.CornerRadius = (float)boxView.CornerRadius.TopLeft; + } - public static void MapBackground(BoxViewHandler handler, BoxView boxView) - { - //IL_0023: Unknown result type (might be due to invalid IL or missing references) - Brush background = ((VisualElement)boxView).Background; - SolidColorBrush val = (SolidColorBrush)(object)((background is SolidColorBrush) ? background : null); - if (val != null && val.Color != null) - { - ((ViewHandler)(object)handler).PlatformView.BackgroundColor = val.Color.ToSKColor(); - ((ViewHandler)(object)handler).PlatformView.Invalidate(); - } - } + public static void MapBackground(BoxViewHandler handler, BoxView boxView) + { + if (boxView.Background is SolidColorBrush solidBrush && solidBrush.Color != null) + { + handler.PlatformView.BackgroundColor = solidBrush.Color.ToSKColor(); + handler.PlatformView.Invalidate(); + } + } - public static void MapBackgroundColor(BoxViewHandler handler, BoxView boxView) - { - //IL_0014: Unknown result type (might be due to invalid IL or missing references) - if (((VisualElement)boxView).BackgroundColor != null) - { - ((ViewHandler)(object)handler).PlatformView.BackgroundColor = ((VisualElement)boxView).BackgroundColor.ToSKColor(); - ((ViewHandler)(object)handler).PlatformView.Invalidate(); - } - } + public static void MapBackgroundColor(BoxViewHandler handler, BoxView boxView) + { + if (boxView.BackgroundColor != null) + { + handler.PlatformView.BackgroundColor = boxView.BackgroundColor.ToSKColor(); + handler.PlatformView.Invalidate(); + } + } } diff --git a/Handlers/ButtonHandler.cs b/Handlers/ButtonHandler.cs index 36823c2..33110d9 100644 --- a/Handlers/ButtonHandler.cs +++ b/Handlers/ButtonHandler.cs @@ -1,153 +1,198 @@ -using System; -using Microsoft.Maui.Graphics; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using Microsoft.Maui.Handlers; +using Microsoft.Maui.Graphics; using SkiaSharp; namespace Microsoft.Maui.Platform.Linux.Handlers; -public class ButtonHandler : ViewHandler +/// +/// Handler for Button on Linux using Skia rendering. +/// Maps IButton interface to SkiaButton platform view. +/// +public partial class ButtonHandler : ViewHandler { - public static IPropertyMapper Mapper = (IPropertyMapper)(object)new PropertyMapper((IPropertyMapper[])(object)new IPropertyMapper[1] { (IPropertyMapper)ViewHandler.ViewMapper }) - { - ["StrokeColor"] = MapStrokeColor, - ["StrokeThickness"] = MapStrokeThickness, - ["CornerRadius"] = MapCornerRadius, - ["Background"] = MapBackground, - ["Padding"] = MapPadding, - ["IsEnabled"] = MapIsEnabled - }; + public static IPropertyMapper Mapper = new PropertyMapper(ViewHandler.ViewMapper) + { + [nameof(IButtonStroke.StrokeColor)] = MapStrokeColor, + [nameof(IButtonStroke.StrokeThickness)] = MapStrokeThickness, + [nameof(IButtonStroke.CornerRadius)] = MapCornerRadius, + [nameof(IView.Background)] = MapBackground, + [nameof(IPadding.Padding)] = MapPadding, + [nameof(IView.IsEnabled)] = MapIsEnabled, + }; - public static CommandMapper CommandMapper = new CommandMapper((CommandMapper)(object)ViewHandler.ViewCommandMapper); + public static CommandMapper CommandMapper = new(ViewHandler.ViewCommandMapper) + { + }; - public ButtonHandler() - : base((IPropertyMapper)(object)Mapper, (CommandMapper)(object)CommandMapper) - { - } + public ButtonHandler() : base(Mapper, CommandMapper) + { + } - public ButtonHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null) - : base((IPropertyMapper)(((object)mapper) ?? ((object)Mapper)), (CommandMapper)(((object)commandMapper) ?? ((object)CommandMapper))) - { - } + public ButtonHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null) + : base(mapper ?? Mapper, commandMapper ?? CommandMapper) + { + } - protected override SkiaButton CreatePlatformView() - { - return new SkiaButton(); - } + protected override SkiaButton CreatePlatformView() + { + var button = new SkiaButton(); + return button; + } - protected override void ConnectHandler(SkiaButton platformView) - { - base.ConnectHandler(platformView); - platformView.Clicked += OnClicked; - platformView.Pressed += OnPressed; - platformView.Released += OnReleased; - if (base.VirtualView != null) - { - MapStrokeColor(this, base.VirtualView); - MapStrokeThickness(this, base.VirtualView); - MapCornerRadius(this, base.VirtualView); - MapBackground(this, base.VirtualView); - MapPadding(this, base.VirtualView); - MapIsEnabled(this, base.VirtualView); - } - } + protected override void ConnectHandler(SkiaButton platformView) + { + base.ConnectHandler(platformView); + platformView.Clicked += OnClicked; + platformView.Pressed += OnPressed; + platformView.Released += OnReleased; - protected override void DisconnectHandler(SkiaButton platformView) - { - platformView.Clicked -= OnClicked; - platformView.Pressed -= OnPressed; - platformView.Released -= OnReleased; - base.DisconnectHandler(platformView); - } + // Manually map all properties on connect since MAUI may not trigger updates + // for properties that were set before handler connection + if (VirtualView != null) + { + MapStrokeColor(this, VirtualView); + MapStrokeThickness(this, VirtualView); + MapCornerRadius(this, VirtualView); + MapBackground(this, VirtualView); + MapPadding(this, VirtualView); + MapIsEnabled(this, VirtualView); + } + } - private void OnClicked(object? sender, EventArgs e) - { - IButton virtualView = base.VirtualView; - if (virtualView != null) - { - virtualView.Clicked(); - } - } + protected override void DisconnectHandler(SkiaButton platformView) + { + platformView.Clicked -= OnClicked; + platformView.Pressed -= OnPressed; + platformView.Released -= OnReleased; + base.DisconnectHandler(platformView); + } - private void OnPressed(object? sender, EventArgs e) - { - IButton virtualView = base.VirtualView; - if (virtualView != null) - { - virtualView.Pressed(); - } - } + private void OnClicked(object? sender, EventArgs e) => VirtualView?.Clicked(); + private void OnPressed(object? sender, EventArgs e) => VirtualView?.Pressed(); + private void OnReleased(object? sender, EventArgs e) => VirtualView?.Released(); - private void OnReleased(object? sender, EventArgs e) - { - IButton virtualView = base.VirtualView; - if (virtualView != null) - { - virtualView.Released(); - } - } + public static void MapStrokeColor(ButtonHandler handler, IButton button) + { + if (handler.PlatformView is null) return; - public static void MapStrokeColor(ButtonHandler handler, IButton button) - { - //IL_001a: Unknown result type (might be due to invalid IL or missing references) - if (((ViewHandler)(object)handler).PlatformView != null) - { - Color strokeColor = ((IButtonStroke)button).StrokeColor; - if (strokeColor != null) - { - ((ViewHandler)(object)handler).PlatformView.BorderColor = strokeColor.ToSKColor(); - } - } - } + var strokeColor = button.StrokeColor; + if (strokeColor is not null) + handler.PlatformView.BorderColor = strokeColor.ToSKColor(); + } - public static void MapStrokeThickness(ButtonHandler handler, IButton button) - { - if (((ViewHandler)(object)handler).PlatformView != null) - { - ((ViewHandler)(object)handler).PlatformView.BorderWidth = (float)((IButtonStroke)button).StrokeThickness; - } - } + public static void MapStrokeThickness(ButtonHandler handler, IButton button) + { + if (handler.PlatformView is null) return; + handler.PlatformView.BorderWidth = (float)button.StrokeThickness; + } - public static void MapCornerRadius(ButtonHandler handler, IButton button) - { - if (((ViewHandler)(object)handler).PlatformView != null) - { - ((ViewHandler)(object)handler).PlatformView.CornerRadius = ((IButtonStroke)button).CornerRadius; - } - } + public static void MapCornerRadius(ButtonHandler handler, IButton button) + { + if (handler.PlatformView is null) return; + handler.PlatformView.CornerRadius = button.CornerRadius; + } - public static void MapBackground(ButtonHandler handler, IButton button) - { - //IL_002c: Unknown result type (might be due to invalid IL or missing references) - if (((ViewHandler)(object)handler).PlatformView != null) - { - Paint background = ((IView)button).Background; - SolidPaint val = (SolidPaint)(object)((background is SolidPaint) ? background : null); - if (val != null && val.Color != null) - { - ((ViewHandler)(object)handler).PlatformView.ButtonBackgroundColor = val.Color.ToSKColor(); - } - } - } + public static void MapBackground(ButtonHandler handler, IButton button) + { + if (handler.PlatformView is null) return; - public static void MapPadding(ButtonHandler handler, IButton button) - { - //IL_000a: Unknown result type (might be due to invalid IL or missing references) - //IL_000f: Unknown result type (might be due to invalid IL or missing references) - //IL_0036: Unknown result type (might be due to invalid IL or missing references) - if (((ViewHandler)(object)handler).PlatformView != null) - { - Thickness padding = ((IPadding)button).Padding; - ((ViewHandler)(object)handler).PlatformView.Padding = new SKRect((float)((Thickness)(ref padding)).Left, (float)((Thickness)(ref padding)).Top, (float)((Thickness)(ref padding)).Right, (float)((Thickness)(ref padding)).Bottom); - } - } + if (button.Background is SolidPaint solidPaint && solidPaint.Color is not null) + { + // Set ButtonBackgroundColor (used for rendering) not base BackgroundColor + handler.PlatformView.ButtonBackgroundColor = solidPaint.Color.ToSKColor(); + } + } - public static void MapIsEnabled(ButtonHandler handler, IButton button) - { - if (((ViewHandler)(object)handler).PlatformView != null) - { - Console.WriteLine($"[ButtonHandler] MapIsEnabled - Text='{((ViewHandler)(object)handler).PlatformView.Text}', IsEnabled={((IView)button).IsEnabled}"); - ((ViewHandler)(object)handler).PlatformView.IsEnabled = ((IView)button).IsEnabled; - ((ViewHandler)(object)handler).PlatformView.Invalidate(); - } - } + public static void MapPadding(ButtonHandler handler, IButton button) + { + if (handler.PlatformView is null) return; + + var padding = button.Padding; + handler.PlatformView.Padding = new SKRect( + (float)padding.Left, + (float)padding.Top, + (float)padding.Right, + (float)padding.Bottom); + } + + public static void MapIsEnabled(ButtonHandler handler, IButton button) + { + if (handler.PlatformView is null) return; + Console.WriteLine($"[ButtonHandler] MapIsEnabled - Text='{handler.PlatformView.Text}', IsEnabled={button.IsEnabled}"); + handler.PlatformView.IsEnabled = button.IsEnabled; + handler.PlatformView.Invalidate(); + } +} + +/// +/// Handler for TextButton on Linux - extends ButtonHandler with text support. +/// Maps ITextButton interface (which includes IText properties). +/// +public partial class TextButtonHandler : ButtonHandler +{ + public static new IPropertyMapper Mapper = + new PropertyMapper(ButtonHandler.Mapper) + { + [nameof(IText.Text)] = MapText, + [nameof(ITextStyle.TextColor)] = MapTextColor, + [nameof(ITextStyle.Font)] = MapFont, + [nameof(ITextStyle.CharacterSpacing)] = MapCharacterSpacing, + }; + + public TextButtonHandler() : base(Mapper) + { + } + + protected override void ConnectHandler(SkiaButton platformView) + { + base.ConnectHandler(platformView); + + // Manually map text properties on connect since MAUI may not trigger updates + // for properties that were set before handler connection + if (VirtualView is ITextButton textButton) + { + MapText(this, textButton); + MapTextColor(this, textButton); + MapFont(this, textButton); + MapCharacterSpacing(this, textButton); + } + } + + public static void MapText(TextButtonHandler handler, ITextButton button) + { + if (handler.PlatformView is null) return; + handler.PlatformView.Text = button.Text ?? string.Empty; + } + + public static void MapTextColor(TextButtonHandler handler, ITextButton button) + { + if (handler.PlatformView is null) return; + + if (button.TextColor is not null) + handler.PlatformView.TextColor = button.TextColor.ToSKColor(); + } + + public static void MapFont(TextButtonHandler handler, ITextButton button) + { + if (handler.PlatformView is null) return; + + var font = button.Font; + if (font.Size > 0) + handler.PlatformView.FontSize = (float)font.Size; + + if (!string.IsNullOrEmpty(font.Family)) + handler.PlatformView.FontFamily = font.Family; + + handler.PlatformView.IsBold = font.Weight >= FontWeight.Bold; + handler.PlatformView.IsItalic = font.Slant == FontSlant.Italic || font.Slant == FontSlant.Oblique; + } + + public static void MapCharacterSpacing(TextButtonHandler handler, ITextButton button) + { + if (handler.PlatformView is null) return; + handler.PlatformView.CharacterSpacing = (float)button.CharacterSpacing; + } } diff --git a/Handlers/CheckBoxHandler.cs b/Handlers/CheckBoxHandler.cs index 8ba2631..cfc966b 100644 --- a/Handlers/CheckBoxHandler.cs +++ b/Handlers/CheckBoxHandler.cs @@ -1,164 +1,116 @@ -using Microsoft.Maui.Controls; -using Microsoft.Maui.Graphics; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using Microsoft.Maui.Handlers; -using Microsoft.Maui.Primitives; +using Microsoft.Maui.Graphics; +using SkiaSharp; namespace Microsoft.Maui.Platform.Linux.Handlers; -public class CheckBoxHandler : ViewHandler +/// +/// Handler for CheckBox on Linux using Skia rendering. +/// Maps ICheckBox interface to SkiaCheckBox platform view. +/// +public partial class CheckBoxHandler : ViewHandler { - public static IPropertyMapper Mapper = (IPropertyMapper)(object)new PropertyMapper((IPropertyMapper[])(object)new IPropertyMapper[1] { (IPropertyMapper)ViewHandler.ViewMapper }) - { - ["IsChecked"] = MapIsChecked, - ["Foreground"] = MapForeground, - ["Background"] = MapBackground, - ["IsEnabled"] = MapIsEnabled, - ["VerticalLayoutAlignment"] = MapVerticalLayoutAlignment, - ["HorizontalLayoutAlignment"] = MapHorizontalLayoutAlignment - }; + public static IPropertyMapper Mapper = new PropertyMapper(ViewHandler.ViewMapper) + { + [nameof(ICheckBox.IsChecked)] = MapIsChecked, + [nameof(ICheckBox.Foreground)] = MapForeground, + [nameof(IView.Background)] = MapBackground, + [nameof(IView.VerticalLayoutAlignment)] = MapVerticalLayoutAlignment, + [nameof(IView.HorizontalLayoutAlignment)] = MapHorizontalLayoutAlignment, + }; - public static CommandMapper CommandMapper = new CommandMapper((CommandMapper)(object)ViewHandler.ViewCommandMapper); + public static CommandMapper CommandMapper = new(ViewHandler.ViewCommandMapper) + { + }; - public CheckBoxHandler() - : base((IPropertyMapper)(object)Mapper, (CommandMapper)(object)CommandMapper) - { - } + public CheckBoxHandler() : base(Mapper, CommandMapper) + { + } - public CheckBoxHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null) - : base((IPropertyMapper)(((object)mapper) ?? ((object)Mapper)), (CommandMapper)(((object)commandMapper) ?? ((object)CommandMapper))) - { - } + public CheckBoxHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null) + : base(mapper ?? Mapper, commandMapper ?? CommandMapper) + { + } - protected override SkiaCheckBox CreatePlatformView() - { - return new SkiaCheckBox(); - } + protected override SkiaCheckBox CreatePlatformView() + { + return new SkiaCheckBox(); + } - protected override void ConnectHandler(SkiaCheckBox platformView) - { - base.ConnectHandler(platformView); - platformView.CheckedChanged += OnCheckedChanged; - } + protected override void ConnectHandler(SkiaCheckBox platformView) + { + base.ConnectHandler(platformView); + platformView.CheckedChanged += OnCheckedChanged; + } - protected override void DisconnectHandler(SkiaCheckBox platformView) - { - platformView.CheckedChanged -= OnCheckedChanged; - base.DisconnectHandler(platformView); - } + protected override void DisconnectHandler(SkiaCheckBox platformView) + { + platformView.CheckedChanged -= OnCheckedChanged; + base.DisconnectHandler(platformView); + } - private void OnCheckedChanged(object? sender, CheckedChangedEventArgs e) - { - if (base.VirtualView != null && base.VirtualView.IsChecked != e.IsChecked) - { - base.VirtualView.IsChecked = e.IsChecked; - } - } + private void OnCheckedChanged(object? sender, Platform.CheckedChangedEventArgs e) + { + if (VirtualView is not null && VirtualView.IsChecked != e.IsChecked) + { + VirtualView.IsChecked = e.IsChecked; + } + } - public static void MapIsChecked(CheckBoxHandler handler, ICheckBox checkBox) - { - if (((ViewHandler)(object)handler).PlatformView != null) - { - ((ViewHandler)(object)handler).PlatformView.IsChecked = checkBox.IsChecked; - } - } + public static void MapIsChecked(CheckBoxHandler handler, ICheckBox checkBox) + { + if (handler.PlatformView is null) return; + handler.PlatformView.IsChecked = checkBox.IsChecked; + } - public static void MapForeground(CheckBoxHandler handler, ICheckBox checkBox) - { - //IL_002c: Unknown result type (might be due to invalid IL or missing references) - if (((ViewHandler)(object)handler).PlatformView != null) - { - Paint foreground = checkBox.Foreground; - SolidPaint val = (SolidPaint)(object)((foreground is SolidPaint) ? foreground : null); - if (val != null && val.Color != null) - { - ((ViewHandler)(object)handler).PlatformView.CheckColor = val.Color.ToSKColor(); - } - } - } + public static void MapForeground(CheckBoxHandler handler, ICheckBox checkBox) + { + if (handler.PlatformView is null) return; - public static void MapBackground(CheckBoxHandler handler, ICheckBox checkBox) - { - //IL_002c: Unknown result type (might be due to invalid IL or missing references) - if (((ViewHandler)(object)handler).PlatformView != null) - { - Paint background = ((IView)checkBox).Background; - SolidPaint val = (SolidPaint)(object)((background is SolidPaint) ? background : null); - if (val != null && val.Color != null) - { - ((ViewHandler)(object)handler).PlatformView.BackgroundColor = val.Color.ToSKColor(); - } - } - } + if (checkBox.Foreground is SolidPaint solidPaint && solidPaint.Color is not null) + { + handler.PlatformView.CheckColor = solidPaint.Color.ToSKColor(); + } + } - public static void MapIsEnabled(CheckBoxHandler handler, ICheckBox checkBox) - { - if (((ViewHandler)(object)handler).PlatformView != null) - { - ((ViewHandler)(object)handler).PlatformView.IsEnabled = ((IView)checkBox).IsEnabled; - } - } + public static void MapBackground(CheckBoxHandler handler, ICheckBox checkBox) + { + if (handler.PlatformView is null) return; - public static void MapVerticalLayoutAlignment(CheckBoxHandler handler, ICheckBox checkBox) - { - //IL_0011: Unknown result type (might be due to invalid IL or missing references) - //IL_0016: Unknown result type (might be due to invalid IL or missing references) - //IL_0017: Unknown result type (might be due to invalid IL or missing references) - //IL_002d: Expected I4, but got Unknown - //IL_0047: Unknown result type (might be due to invalid IL or missing references) - //IL_004c: Unknown result type (might be due to invalid IL or missing references) - //IL_002f: Unknown result type (might be due to invalid IL or missing references) - //IL_0034: Unknown result type (might be due to invalid IL or missing references) - //IL_0037: Unknown result type (might be due to invalid IL or missing references) - //IL_003c: Unknown result type (might be due to invalid IL or missing references) - //IL_003f: Unknown result type (might be due to invalid IL or missing references) - //IL_0044: Unknown result type (might be due to invalid IL or missing references) - //IL_0056: Unknown result type (might be due to invalid IL or missing references) - //IL_004f: Unknown result type (might be due to invalid IL or missing references) - //IL_0054: Unknown result type (might be due to invalid IL or missing references) - if (((ViewHandler)(object)handler).PlatformView != null) - { - SkiaCheckBox platformView = ((ViewHandler)(object)handler).PlatformView; - LayoutAlignment verticalLayoutAlignment = ((IView)checkBox).VerticalLayoutAlignment; - platformView.VerticalOptions = (LayoutOptions)((int)verticalLayoutAlignment switch - { - 1 => LayoutOptions.Start, - 2 => LayoutOptions.Center, - 3 => LayoutOptions.End, - 0 => LayoutOptions.Fill, - _ => LayoutOptions.Fill, - }); - } - } + if (checkBox.Background is SolidPaint solidPaint && solidPaint.Color is not null) + { + handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor(); + } + } - public static void MapHorizontalLayoutAlignment(CheckBoxHandler handler, ICheckBox checkBox) - { - //IL_0011: Unknown result type (might be due to invalid IL or missing references) - //IL_0016: Unknown result type (might be due to invalid IL or missing references) - //IL_0017: Unknown result type (might be due to invalid IL or missing references) - //IL_002d: Expected I4, but got Unknown - //IL_0047: Unknown result type (might be due to invalid IL or missing references) - //IL_004c: Unknown result type (might be due to invalid IL or missing references) - //IL_002f: Unknown result type (might be due to invalid IL or missing references) - //IL_0034: Unknown result type (might be due to invalid IL or missing references) - //IL_0037: Unknown result type (might be due to invalid IL or missing references) - //IL_003c: Unknown result type (might be due to invalid IL or missing references) - //IL_003f: Unknown result type (might be due to invalid IL or missing references) - //IL_0044: Unknown result type (might be due to invalid IL or missing references) - //IL_0056: Unknown result type (might be due to invalid IL or missing references) - //IL_004f: Unknown result type (might be due to invalid IL or missing references) - //IL_0054: Unknown result type (might be due to invalid IL or missing references) - if (((ViewHandler)(object)handler).PlatformView != null) - { - SkiaCheckBox platformView = ((ViewHandler)(object)handler).PlatformView; - LayoutAlignment horizontalLayoutAlignment = ((IView)checkBox).HorizontalLayoutAlignment; - platformView.HorizontalOptions = (LayoutOptions)((int)horizontalLayoutAlignment switch - { - 1 => LayoutOptions.Start, - 2 => LayoutOptions.Center, - 3 => LayoutOptions.End, - 0 => LayoutOptions.Fill, - _ => LayoutOptions.Start, - }); - } - } + public static void MapVerticalLayoutAlignment(CheckBoxHandler handler, ICheckBox checkBox) + { + if (handler.PlatformView is null) return; + + handler.PlatformView.VerticalOptions = checkBox.VerticalLayoutAlignment switch + { + Primitives.LayoutAlignment.Start => LayoutOptions.Start, + Primitives.LayoutAlignment.Center => LayoutOptions.Center, + Primitives.LayoutAlignment.End => LayoutOptions.End, + Primitives.LayoutAlignment.Fill => LayoutOptions.Fill, + _ => LayoutOptions.Fill + }; + } + + public static void MapHorizontalLayoutAlignment(CheckBoxHandler handler, ICheckBox checkBox) + { + if (handler.PlatformView is null) return; + + handler.PlatformView.HorizontalOptions = checkBox.HorizontalLayoutAlignment switch + { + Primitives.LayoutAlignment.Start => LayoutOptions.Start, + Primitives.LayoutAlignment.Center => LayoutOptions.Center, + Primitives.LayoutAlignment.End => LayoutOptions.End, + Primitives.LayoutAlignment.Fill => LayoutOptions.Fill, + _ => LayoutOptions.Start + }; + } } diff --git a/Handlers/CollectionViewHandler.cs b/Handlers/CollectionViewHandler.cs index 7271d94..e76a4a6 100644 --- a/Handlers/CollectionViewHandler.cs +++ b/Handlers/CollectionViewHandler.cs @@ -1,458 +1,374 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.Maui.Controls; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using Microsoft.Maui.Handlers; -using Microsoft.Maui.Platform.Linux.Hosting; +using Microsoft.Maui.Graphics; +using Microsoft.Maui.Controls; +using Microsoft.Maui.Platform; +using SkiaSharp; namespace Microsoft.Maui.Platform.Linux.Handlers; -public class CollectionViewHandler : ViewHandler +/// +/// Handler for CollectionView on Linux using Skia rendering. +/// Maps CollectionView to SkiaCollectionView platform view. +/// +public partial class CollectionViewHandler : ViewHandler { - private bool _isUpdatingSelection; + private bool _isUpdatingSelection; - public static IPropertyMapper Mapper = (IPropertyMapper)(object)new PropertyMapper((IPropertyMapper[])(object)new IPropertyMapper[1] { (IPropertyMapper)ViewHandler.ViewMapper }) - { - ["ItemsSource"] = MapItemsSource, - ["ItemTemplate"] = MapItemTemplate, - ["EmptyView"] = MapEmptyView, - ["HorizontalScrollBarVisibility"] = MapHorizontalScrollBarVisibility, - ["VerticalScrollBarVisibility"] = MapVerticalScrollBarVisibility, - ["SelectedItem"] = MapSelectedItem, - ["SelectedItems"] = MapSelectedItems, - ["SelectionMode"] = MapSelectionMode, - ["Header"] = MapHeader, - ["Footer"] = MapFooter, - ["ItemsLayout"] = MapItemsLayout, - ["Background"] = MapBackground, - ["BackgroundColor"] = MapBackgroundColor - }; + public static IPropertyMapper Mapper = + new PropertyMapper(ViewHandler.ViewMapper) + { + // ItemsView properties + [nameof(ItemsView.ItemsSource)] = MapItemsSource, + [nameof(ItemsView.ItemTemplate)] = MapItemTemplate, + [nameof(ItemsView.EmptyView)] = MapEmptyView, + [nameof(ItemsView.HorizontalScrollBarVisibility)] = MapHorizontalScrollBarVisibility, + [nameof(ItemsView.VerticalScrollBarVisibility)] = MapVerticalScrollBarVisibility, - public static CommandMapper CommandMapper = new CommandMapper((CommandMapper)(object)ViewHandler.ViewCommandMapper) { ["ScrollTo"] = MapScrollTo }; + // SelectableItemsView properties + [nameof(SelectableItemsView.SelectedItem)] = MapSelectedItem, + [nameof(SelectableItemsView.SelectedItems)] = MapSelectedItems, + [nameof(SelectableItemsView.SelectionMode)] = MapSelectionMode, - public CollectionViewHandler() - : base((IPropertyMapper)(object)Mapper, (CommandMapper)(object)CommandMapper) - { - } + // StructuredItemsView properties + [nameof(StructuredItemsView.Header)] = MapHeader, + [nameof(StructuredItemsView.Footer)] = MapFooter, + [nameof(StructuredItemsView.ItemsLayout)] = MapItemsLayout, - public CollectionViewHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null) - : base((IPropertyMapper)(((object)mapper) ?? ((object)Mapper)), (CommandMapper)(((object)commandMapper) ?? ((object)CommandMapper))) - { - } + [nameof(IView.Background)] = MapBackground, + [nameof(CollectionView.BackgroundColor)] = MapBackgroundColor, + }; - protected override SkiaCollectionView CreatePlatformView() - { - return new SkiaCollectionView(); - } + public static CommandMapper CommandMapper = + new(ViewHandler.ViewCommandMapper) + { + ["ScrollTo"] = MapScrollTo, + }; - protected override void ConnectHandler(SkiaCollectionView platformView) - { - base.ConnectHandler(platformView); - platformView.SelectionChanged += OnSelectionChanged; - platformView.Scrolled += OnScrolled; - platformView.ItemTapped += OnItemTapped; - } + public CollectionViewHandler() : base(Mapper, CommandMapper) + { + } - protected override void DisconnectHandler(SkiaCollectionView platformView) - { - platformView.SelectionChanged -= OnSelectionChanged; - platformView.Scrolled -= OnScrolled; - platformView.ItemTapped -= OnItemTapped; - base.DisconnectHandler(platformView); - } + public CollectionViewHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null) + : base(mapper ?? Mapper, commandMapper ?? CommandMapper) + { + } - private void OnSelectionChanged(object? sender, CollectionSelectionChangedEventArgs e) - { - //IL_001f: Unknown result type (might be due to invalid IL or missing references) - //IL_0025: Invalid comparison between Unknown and I4 - //IL_005a: Unknown result type (might be due to invalid IL or missing references) - //IL_0060: Invalid comparison between Unknown and I4 - if (base.VirtualView == null || _isUpdatingSelection) - { - return; - } - try - { - _isUpdatingSelection = true; - if ((int)((SelectableItemsView)base.VirtualView).SelectionMode == 1) - { - object obj = e.CurrentSelection.FirstOrDefault(); - if (!object.Equals(((SelectableItemsView)base.VirtualView).SelectedItem, obj)) - { - ((SelectableItemsView)base.VirtualView).SelectedItem = obj; - } - } - else - { - if ((int)((SelectableItemsView)base.VirtualView).SelectionMode != 2) - { - return; - } - ((SelectableItemsView)base.VirtualView).SelectedItems.Clear(); - { - foreach (object item in e.CurrentSelection) - { - ((SelectableItemsView)base.VirtualView).SelectedItems.Add(item); - } - return; - } - } - } - finally - { - _isUpdatingSelection = false; - } - } + protected override SkiaCollectionView CreatePlatformView() + { + return new SkiaCollectionView(); + } - private void OnScrolled(object? sender, ItemsScrolledEventArgs e) - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - //IL_0010: Unknown result type (might be due to invalid IL or missing references) - //IL_001d: Unknown result type (might be due to invalid IL or missing references) - //IL_002c: Unknown result type (might be due to invalid IL or missing references) - //IL_003b: Unknown result type (might be due to invalid IL or missing references) - //IL_004f: Expected O, but got Unknown - CollectionView virtualView = base.VirtualView; - if (virtualView != null) - { - ((ItemsView)virtualView).SendScrolled(new ItemsViewScrolledEventArgs - { - VerticalOffset = e.ScrollOffset, - VerticalDelta = 0.0, - HorizontalOffset = 0.0, - HorizontalDelta = 0.0 - }); - } - } + protected override void ConnectHandler(SkiaCollectionView platformView) + { + base.ConnectHandler(platformView); + platformView.SelectionChanged += OnSelectionChanged; + platformView.Scrolled += OnScrolled; + platformView.ItemTapped += OnItemTapped; + } - private void OnItemTapped(object? sender, ItemsViewItemTappedEventArgs e) - { - //IL_0068: Unknown result type (might be due to invalid IL or missing references) - //IL_01d2: Unknown result type (might be due to invalid IL or missing references) - //IL_01d8: Invalid comparison between Unknown and I4 - //IL_01f3: Unknown result type (might be due to invalid IL or missing references) - //IL_01f9: Invalid comparison between Unknown and I4 - if (base.VirtualView == null || _isUpdatingSelection) - { - return; - } - try - { - _isUpdatingSelection = true; - Console.WriteLine($"[CollectionViewHandler] OnItemTapped index={e.Index}, item={e.Item}, SelectionMode={((SelectableItemsView)base.VirtualView).SelectionMode}"); - SkiaView skiaView = base.PlatformView?.GetItemView(e.Index); - Console.WriteLine($"[CollectionViewHandler] GetItemView({e.Index}) returned: {((object)skiaView)?.GetType().Name ?? "null"}, MauiView={((object)skiaView?.MauiView)?.GetType().Name ?? "null"}"); - if (skiaView?.MauiView != null) - { - Console.WriteLine($"[CollectionViewHandler] Found MauiView: {((object)skiaView.MauiView).GetType().Name}, GestureRecognizers={skiaView.MauiView.GestureRecognizers?.Count ?? 0}"); - if (GestureManager.ProcessTap(skiaView.MauiView, 0.0, 0.0)) - { - Console.WriteLine("[CollectionViewHandler] Gesture processed successfully"); - return; - } - } - if ((int)((SelectableItemsView)base.VirtualView).SelectionMode == 1) - { - ((SelectableItemsView)base.VirtualView).SelectedItem = e.Item; - } - else if ((int)((SelectableItemsView)base.VirtualView).SelectionMode == 2) - { - if (((SelectableItemsView)base.VirtualView).SelectedItems.Contains(e.Item)) - { - ((SelectableItemsView)base.VirtualView).SelectedItems.Remove(e.Item); - } - else - { - ((SelectableItemsView)base.VirtualView).SelectedItems.Add(e.Item); - } - } - } - finally - { - _isUpdatingSelection = false; - } - } + protected override void DisconnectHandler(SkiaCollectionView platformView) + { + platformView.SelectionChanged -= OnSelectionChanged; + platformView.Scrolled -= OnScrolled; + platformView.ItemTapped -= OnItemTapped; + base.DisconnectHandler(platformView); + } - public static void MapItemsSource(CollectionViewHandler handler, CollectionView collectionView) - { - if (((ViewHandler)(object)handler).PlatformView != null) - { - ((ViewHandler)(object)handler).PlatformView.ItemsSource = ((ItemsView)collectionView).ItemsSource; - } - } + private void OnSelectionChanged(object? sender, CollectionSelectionChangedEventArgs e) + { + if (VirtualView is null || _isUpdatingSelection) return; - public static void MapItemTemplate(CollectionViewHandler handler, CollectionView collectionView) - { - if (((ViewHandler)(object)handler).PlatformView == null || ((ElementHandler)handler).MauiContext == null) - { - return; - } - DataTemplate template = ((ItemsView)collectionView).ItemTemplate; - if (template != null) - { - ((ViewHandler)(object)handler).PlatformView.ItemViewCreator = delegate(object item) - { - try - { - object obj = ((ElementTemplate)template).CreateContent(); - View val = (View)((obj is View) ? obj : null); - if (val != null) - { - ((BindableObject)val).BindingContext = item; - PropagateBindingContext(val, item); - if (((VisualElement)val).Handler == null && ((ElementHandler)handler).MauiContext != null) - { - ((VisualElement)val).Handler = ((IView)(object)val).ToViewHandler(((ElementHandler)handler).MauiContext); - } - IViewHandler handler2 = ((VisualElement)val).Handler; - if (((handler2 != null) ? ((IElementHandler)handler2).PlatformView : null) is SkiaView skiaView) - { - skiaView.MauiView = val; - Console.WriteLine($"[CollectionViewHandler.ItemViewCreator] Set MauiView={((object)val).GetType().Name} on {((object)skiaView).GetType().Name}, GestureRecognizers={val.GestureRecognizers?.Count ?? 0}"); - return skiaView; - } - } - else - { - ViewCell val2 = (ViewCell)((obj is ViewCell) ? obj : null); - if (val2 != null) - { - ((BindableObject)val2).BindingContext = item; - View view = val2.View; - if (view != null) - { - if (((VisualElement)view).Handler == null && ((ElementHandler)handler).MauiContext != null) - { - ((VisualElement)view).Handler = ((IView)(object)view).ToViewHandler(((ElementHandler)handler).MauiContext); - } - IViewHandler handler3 = ((VisualElement)view).Handler; - if (((handler3 != null) ? ((IElementHandler)handler3).PlatformView : null) is SkiaView result) - { - return result; - } - } - } - } - } - catch - { - } - return (SkiaView?)null; - }; - } - ((ViewHandler)(object)handler).PlatformView.Invalidate(); - } + try + { + _isUpdatingSelection = true; - public static void MapEmptyView(CollectionViewHandler handler, CollectionView collectionView) - { - if (((ViewHandler)(object)handler).PlatformView != null) - { - ((ViewHandler)(object)handler).PlatformView.EmptyView = ((ItemsView)collectionView).EmptyView; - if (((ItemsView)collectionView).EmptyView is string emptyViewText) - { - ((ViewHandler)(object)handler).PlatformView.EmptyViewText = emptyViewText; - } - } - } + // Update virtual view selection + if (VirtualView.SelectionMode == SelectionMode.Single) + { + var newItem = e.CurrentSelection.FirstOrDefault(); + if (!Equals(VirtualView.SelectedItem, newItem)) + { + VirtualView.SelectedItem = newItem; + } + } + else if (VirtualView.SelectionMode == SelectionMode.Multiple) + { + // Clear and update selected items + VirtualView.SelectedItems.Clear(); + foreach (var item in e.CurrentSelection) + { + VirtualView.SelectedItems.Add(item); + } + } + } + finally + { + _isUpdatingSelection = false; + } + } - public static void MapHorizontalScrollBarVisibility(CollectionViewHandler handler, CollectionView collectionView) - { - //IL_0010: Unknown result type (might be due to invalid IL or missing references) - //IL_001a: Expected I4, but got Unknown - if (((ViewHandler)(object)handler).PlatformView != null) - { - ((ViewHandler)(object)handler).PlatformView.HorizontalScrollBarVisibility = (ScrollBarVisibility)((ItemsView)collectionView).HorizontalScrollBarVisibility; - } - } + private void OnScrolled(object? sender, ItemsScrolledEventArgs e) + { + VirtualView?.SendScrolled(new ItemsViewScrolledEventArgs + { + VerticalOffset = e.ScrollOffset, + VerticalDelta = 0, + HorizontalOffset = 0, + HorizontalDelta = 0 + }); + } - public static void MapVerticalScrollBarVisibility(CollectionViewHandler handler, CollectionView collectionView) - { - //IL_0010: Unknown result type (might be due to invalid IL or missing references) - //IL_001a: Expected I4, but got Unknown - if (((ViewHandler)(object)handler).PlatformView != null) - { - ((ViewHandler)(object)handler).PlatformView.VerticalScrollBarVisibility = (ScrollBarVisibility)((ItemsView)collectionView).VerticalScrollBarVisibility; - } - } + private void OnItemTapped(object? sender, ItemsViewItemTappedEventArgs e) + { + // Item tap is handled through selection + } - public static void MapSelectedItem(CollectionViewHandler handler, CollectionView collectionView) - { - if (((ViewHandler)(object)handler).PlatformView == null || handler._isUpdatingSelection) - { - return; - } - try - { - handler._isUpdatingSelection = true; - if (!object.Equals(((ViewHandler)(object)handler).PlatformView.SelectedItem, ((SelectableItemsView)collectionView).SelectedItem)) - { - ((ViewHandler)(object)handler).PlatformView.SelectedItem = ((SelectableItemsView)collectionView).SelectedItem; - } - } - finally - { - handler._isUpdatingSelection = false; - } - } + public static void MapItemsSource(CollectionViewHandler handler, CollectionView collectionView) + { + if (handler.PlatformView is null) return; + handler.PlatformView.ItemsSource = collectionView.ItemsSource; + } - public static void MapSelectedItems(CollectionViewHandler handler, CollectionView collectionView) - { - if (((ViewHandler)(object)handler).PlatformView == null || handler._isUpdatingSelection) - { - return; - } - try - { - handler._isUpdatingSelection = true; - IList selectedItems = ((SelectableItemsView)collectionView).SelectedItems; - if (selectedItems != null && selectedItems.Count > 0) - { - ((ViewHandler)(object)handler).PlatformView.SelectedItem = selectedItems.First(); - } - } - finally - { - handler._isUpdatingSelection = false; - } - } + public static void MapItemTemplate(CollectionViewHandler handler, CollectionView collectionView) + { + if (handler.PlatformView is null || handler.MauiContext is null) return; - public static void MapSelectionMode(CollectionViewHandler handler, CollectionView collectionView) - { - //IL_0011: Unknown result type (might be due to invalid IL or missing references) - //IL_0016: Unknown result type (might be due to invalid IL or missing references) - //IL_0017: Unknown result type (might be due to invalid IL or missing references) - //IL_0029: Expected I4, but got Unknown - if (((ViewHandler)(object)handler).PlatformView != null) - { - SkiaCollectionView platformView = ((ViewHandler)(object)handler).PlatformView; - SelectionMode selectionMode = ((SelectableItemsView)collectionView).SelectionMode; - platformView.SelectionMode = (int)selectionMode switch - { - 0 => SkiaSelectionMode.None, - 1 => SkiaSelectionMode.Single, - 2 => SkiaSelectionMode.Multiple, - _ => SkiaSelectionMode.None, - }; - } - } + var template = collectionView.ItemTemplate; + if (template != null) + { + // Set up a renderer that creates views from the DataTemplate + handler.PlatformView.ItemViewCreator = (item) => + { + try + { + // Create view from template + var content = template.CreateContent(); + if (content is View view) + { + // Set binding context FIRST so bindings evaluate + view.BindingContext = item; - public static void MapHeader(CollectionViewHandler handler, CollectionView collectionView) - { - if (((ViewHandler)(object)handler).PlatformView != null) - { - ((ViewHandler)(object)handler).PlatformView.Header = ((StructuredItemsView)collectionView).Header; - } - } + // Force binding evaluation by accessing the visual tree + // This ensures child bindings are evaluated before handler creation + PropagateBindingContext(view, item); - public static void MapFooter(CollectionViewHandler handler, CollectionView collectionView) - { - if (((ViewHandler)(object)handler).PlatformView != null) - { - ((ViewHandler)(object)handler).PlatformView.Footer = ((StructuredItemsView)collectionView).Footer; - } - } + // Create handler for the view + if (view.Handler == null && handler.MauiContext != null) + { + view.Handler = view.ToHandler(handler.MauiContext); + } - public static void MapItemsLayout(CollectionViewHandler handler, CollectionView collectionView) - { - //IL_0021: Unknown result type (might be due to invalid IL or missing references) - //IL_0061: Unknown result type (might be due to invalid IL or missing references) - if (((ViewHandler)(object)handler).PlatformView == null) - { - return; - } - IItemsLayout itemsLayout = ((StructuredItemsView)collectionView).ItemsLayout; - LinearItemsLayout val = (LinearItemsLayout)(object)((itemsLayout is LinearItemsLayout) ? itemsLayout : null); - if (val != null) - { - ((ViewHandler)(object)handler).PlatformView.Orientation = (((int)((ItemsLayout)val).Orientation != 0) ? ItemsLayoutOrientation.Horizontal : ItemsLayoutOrientation.Vertical); - ((ViewHandler)(object)handler).PlatformView.SpanCount = 1; - ((ViewHandler)(object)handler).PlatformView.ItemSpacing = (float)val.ItemSpacing; - return; - } - GridItemsLayout val2 = (GridItemsLayout)(object)((itemsLayout is GridItemsLayout) ? itemsLayout : null); - if (val2 != null) - { - ((ViewHandler)(object)handler).PlatformView.Orientation = (((int)((ItemsLayout)val2).Orientation != 0) ? ItemsLayoutOrientation.Horizontal : ItemsLayoutOrientation.Vertical); - ((ViewHandler)(object)handler).PlatformView.SpanCount = val2.Span; - ((ViewHandler)(object)handler).PlatformView.ItemSpacing = (float)val2.VerticalItemSpacing; - } - } + if (view.Handler?.PlatformView is SkiaView skiaView) + { + return skiaView; + } + } + else if (content is ViewCell cell) + { + cell.BindingContext = item; + var cellView = cell.View; + if (cellView != null) + { + if (cellView.Handler == null && handler.MauiContext != null) + { + cellView.Handler = cellView.ToHandler(handler.MauiContext); + } - public static void MapBackground(CollectionViewHandler handler, CollectionView collectionView) - { - //IL_002d: Unknown result type (might be due to invalid IL or missing references) - if (((ViewHandler)(object)handler).PlatformView != null && ((VisualElement)collectionView).BackgroundColor == null) - { - Brush background = ((VisualElement)collectionView).Background; - SolidColorBrush val = (SolidColorBrush)(object)((background is SolidColorBrush) ? background : null); - if (val != null) - { - ((ViewHandler)(object)handler).PlatformView.BackgroundColor = val.Color.ToSKColor(); - } - } - } + if (cellView.Handler?.PlatformView is SkiaView skiaView) + { + return skiaView; + } + } + } + } + catch + { + // Ignore template creation errors + } + return null; + }; + } - public static void MapBackgroundColor(CollectionViewHandler handler, CollectionView collectionView) - { - //IL_001d: Unknown result type (might be due to invalid IL or missing references) - if (((ViewHandler)(object)handler).PlatformView != null && ((VisualElement)collectionView).BackgroundColor != null) - { - ((ViewHandler)(object)handler).PlatformView.BackgroundColor = ((VisualElement)collectionView).BackgroundColor.ToSKColor(); - } - } + handler.PlatformView.Invalidate(); + } - public static void MapScrollTo(CollectionViewHandler handler, CollectionView collectionView, object? args) - { - //IL_0014: Unknown result type (might be due to invalid IL or missing references) - //IL_001a: Invalid comparison between Unknown and I4 - if (((ViewHandler)(object)handler).PlatformView == null) - { - return; - } - ScrollToRequestEventArgs e = (ScrollToRequestEventArgs)((args is ScrollToRequestEventArgs) ? args : null); - if (e != null) - { - if ((int)e.Mode == 1) - { - ((ViewHandler)(object)handler).PlatformView.ScrollToIndex(e.Index, e.IsAnimated); - } - else if (e.Item != null) - { - ((ViewHandler)(object)handler).PlatformView.ScrollToItem(e.Item, e.IsAnimated); - } - } - } + public static void MapEmptyView(CollectionViewHandler handler, CollectionView collectionView) + { + if (handler.PlatformView is null) return; - private static void PropagateBindingContext(View view, object? bindingContext) - { - ((BindableObject)view).BindingContext = bindingContext; - Layout val = (Layout)(object)((view is Layout) ? view : null); - if (val != null) - { - foreach (IView child in val.Children) - { - View val2 = (View)(object)((child is View) ? child : null); - if (val2 != null) - { - PropagateBindingContext(val2, bindingContext); - } - } - return; - } - ContentView val3 = (ContentView)(object)((view is ContentView) ? view : null); - if (val3 != null && val3.Content != null) - { - PropagateBindingContext(val3.Content, bindingContext); - return; - } - Border val4 = (Border)(object)((view is Border) ? view : null); - if (val4 != null) - { - View content = val4.Content; - if (content != null) - { - PropagateBindingContext(content, bindingContext); - } - } - } + handler.PlatformView.EmptyView = collectionView.EmptyView; + if (collectionView.EmptyView is string text) + { + handler.PlatformView.EmptyViewText = text; + } + } + + public static void MapHorizontalScrollBarVisibility(CollectionViewHandler handler, CollectionView collectionView) + { + if (handler.PlatformView is null) return; + handler.PlatformView.HorizontalScrollBarVisibility = (ScrollBarVisibility)collectionView.HorizontalScrollBarVisibility; + } + + public static void MapVerticalScrollBarVisibility(CollectionViewHandler handler, CollectionView collectionView) + { + if (handler.PlatformView is null) return; + handler.PlatformView.VerticalScrollBarVisibility = (ScrollBarVisibility)collectionView.VerticalScrollBarVisibility; + } + + public static void MapSelectedItem(CollectionViewHandler handler, CollectionView collectionView) + { + if (handler.PlatformView is null || handler._isUpdatingSelection) return; + + try + { + handler._isUpdatingSelection = true; + if (!Equals(handler.PlatformView.SelectedItem, collectionView.SelectedItem)) + { + handler.PlatformView.SelectedItem = collectionView.SelectedItem; + } + } + finally + { + handler._isUpdatingSelection = false; + } + } + + public static void MapSelectedItems(CollectionViewHandler handler, CollectionView collectionView) + { + if (handler.PlatformView is null || handler._isUpdatingSelection) return; + + try + { + handler._isUpdatingSelection = true; + + // Sync selected items + var selectedItems = collectionView.SelectedItems; + if (selectedItems != null && selectedItems.Count > 0) + { + handler.PlatformView.SelectedItem = selectedItems.First(); + } + } + finally + { + handler._isUpdatingSelection = false; + } + } + + public static void MapSelectionMode(CollectionViewHandler handler, CollectionView collectionView) + { + if (handler.PlatformView is null) return; + + handler.PlatformView.SelectionMode = collectionView.SelectionMode switch + { + SelectionMode.None => SkiaSelectionMode.None, + SelectionMode.Single => SkiaSelectionMode.Single, + SelectionMode.Multiple => SkiaSelectionMode.Multiple, + _ => SkiaSelectionMode.None + }; + } + + public static void MapHeader(CollectionViewHandler handler, CollectionView collectionView) + { + if (handler.PlatformView is null) return; + handler.PlatformView.Header = collectionView.Header; + } + + public static void MapFooter(CollectionViewHandler handler, CollectionView collectionView) + { + if (handler.PlatformView is null) return; + handler.PlatformView.Footer = collectionView.Footer; + } + + public static void MapItemsLayout(CollectionViewHandler handler, CollectionView collectionView) + { + if (handler.PlatformView is null) return; + + var layout = collectionView.ItemsLayout; + if (layout is LinearItemsLayout linearLayout) + { + handler.PlatformView.Orientation = linearLayout.Orientation == Controls.ItemsLayoutOrientation.Vertical + ? Platform.ItemsLayoutOrientation.Vertical + : Platform.ItemsLayoutOrientation.Horizontal; + handler.PlatformView.SpanCount = 1; + handler.PlatformView.ItemSpacing = (float)linearLayout.ItemSpacing; + } + else if (layout is GridItemsLayout gridLayout) + { + handler.PlatformView.Orientation = gridLayout.Orientation == Controls.ItemsLayoutOrientation.Vertical + ? Platform.ItemsLayoutOrientation.Vertical + : Platform.ItemsLayoutOrientation.Horizontal; + handler.PlatformView.SpanCount = gridLayout.Span; + handler.PlatformView.ItemSpacing = (float)gridLayout.VerticalItemSpacing; + } + } + + public static void MapBackground(CollectionViewHandler handler, CollectionView collectionView) + { + if (handler.PlatformView is null) return; + + // Don't override if BackgroundColor is explicitly set + if (collectionView.BackgroundColor is not null) + return; + + if (collectionView.Background is SolidColorBrush solidBrush) + { + handler.PlatformView.BackgroundColor = solidBrush.Color.ToSKColor(); + } + } + + public static void MapBackgroundColor(CollectionViewHandler handler, CollectionView collectionView) + { + if (handler.PlatformView is null) return; + + if (collectionView.BackgroundColor is not null) + { + handler.PlatformView.BackgroundColor = collectionView.BackgroundColor.ToSKColor(); + } + } + + public static void MapScrollTo(CollectionViewHandler handler, CollectionView collectionView, object? args) + { + if (handler.PlatformView is null || args is not ScrollToRequestEventArgs scrollArgs) + return; + + if (scrollArgs.Mode == ScrollToMode.Position) + { + handler.PlatformView.ScrollToIndex(scrollArgs.Index, scrollArgs.IsAnimated); + } + else if (scrollArgs.Item != null) + { + handler.PlatformView.ScrollToItem(scrollArgs.Item, scrollArgs.IsAnimated); + } + } + + /// + /// Recursively propagates binding context to all child views to force binding evaluation. + /// + private static void PropagateBindingContext(View view, object? bindingContext) + { + view.BindingContext = bindingContext; + + // Propagate to children + if (view is Layout layout) + { + foreach (var child in layout.Children) + { + if (child is View childView) + { + PropagateBindingContext(childView, bindingContext); + } + } + } + else if (view is ContentView contentView && contentView.Content != null) + { + PropagateBindingContext(contentView.Content, bindingContext); + } + else if (view is Border border && border.Content is View borderContent) + { + PropagateBindingContext(borderContent, bindingContext); + } + } } diff --git a/Handlers/ContentPageHandler.cs b/Handlers/ContentPageHandler.cs deleted file mode 100644 index 22de5d2..0000000 --- a/Handlers/ContentPageHandler.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System; -using Microsoft.Maui.Controls; -using Microsoft.Maui.Handlers; -using Microsoft.Maui.Platform.Linux.Hosting; - -namespace Microsoft.Maui.Platform.Linux.Handlers; - -public class ContentPageHandler : PageHandler -{ - public new static IPropertyMapper Mapper = (IPropertyMapper)(object)new PropertyMapper((IPropertyMapper[])(object)new IPropertyMapper[1] { (IPropertyMapper)PageHandler.Mapper }) { ["Content"] = MapContent }; - - public new static CommandMapper CommandMapper = new CommandMapper((CommandMapper)(object)PageHandler.CommandMapper); - - public ContentPageHandler() - : base((IPropertyMapper?)(object)Mapper, (CommandMapper?)(object)CommandMapper) - { - } - - public ContentPageHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null) - : base((IPropertyMapper?)(((object)mapper) ?? ((object)Mapper)), (CommandMapper?)(((object)commandMapper) ?? ((object)CommandMapper))) - { - } - - protected override SkiaPage CreatePlatformView() - { - return new SkiaContentPage(); - } - - public static void MapContent(ContentPageHandler handler, ContentPage page) - { - if (((ViewHandler)(object)handler).PlatformView == null || ((ElementHandler)handler).MauiContext == null) - { - return; - } - View content = page.Content; - if (content != null) - { - if (((VisualElement)content).Handler == null) - { - Console.WriteLine("[ContentPageHandler] Creating handler for content: " + ((object)content).GetType().Name); - ((VisualElement)content).Handler = ((IView)(object)content).ToViewHandler(((ElementHandler)handler).MauiContext); - } - IViewHandler handler2 = ((VisualElement)content).Handler; - if (((handler2 != null) ? ((IElementHandler)handler2).PlatformView : null) is SkiaView skiaView) - { - Console.WriteLine("[ContentPageHandler] Setting content: " + ((object)skiaView).GetType().Name); - ((ViewHandler)(object)handler).PlatformView.Content = skiaView; - } - else - { - IViewHandler handler3 = ((VisualElement)content).Handler; - Console.WriteLine("[ContentPageHandler] Content handler PlatformView is not SkiaView: " + (((handler3 == null) ? null : ((IElementHandler)handler3).PlatformView?.GetType().Name) ?? "null")); - } - } - else - { - ((ViewHandler)(object)handler).PlatformView.Content = null; - } - } -} diff --git a/Handlers/DatePickerHandler.cs b/Handlers/DatePickerHandler.cs index 9112ff7..f7b494d 100644 --- a/Handlers/DatePickerHandler.cs +++ b/Handlers/DatePickerHandler.cs @@ -1,133 +1,114 @@ -using System; -using Microsoft.Maui.Controls; -using Microsoft.Maui.Graphics; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using Microsoft.Maui.Handlers; +using Microsoft.Maui.Graphics; +using Microsoft.Maui.Controls; +using Microsoft.Maui.Platform; using SkiaSharp; namespace Microsoft.Maui.Platform.Linux.Handlers; -public class DatePickerHandler : ViewHandler +/// +/// Handler for DatePicker on Linux using Skia rendering. +/// +public partial class DatePickerHandler : ViewHandler { - public static IPropertyMapper Mapper = (IPropertyMapper)(object)new PropertyMapper((IPropertyMapper[])(object)new IPropertyMapper[1] { (IPropertyMapper)ViewHandler.ViewMapper }) - { - ["Date"] = MapDate, - ["MinimumDate"] = MapMinimumDate, - ["MaximumDate"] = MapMaximumDate, - ["Format"] = MapFormat, - ["TextColor"] = MapTextColor, - ["CharacterSpacing"] = MapCharacterSpacing, - ["Background"] = MapBackground - }; + public static IPropertyMapper Mapper = + new PropertyMapper(ViewHandler.ViewMapper) + { + [nameof(IDatePicker.Date)] = MapDate, + [nameof(IDatePicker.MinimumDate)] = MapMinimumDate, + [nameof(IDatePicker.MaximumDate)] = MapMaximumDate, + [nameof(IDatePicker.Format)] = MapFormat, + [nameof(IDatePicker.TextColor)] = MapTextColor, + [nameof(IDatePicker.CharacterSpacing)] = MapCharacterSpacing, + [nameof(IView.Background)] = MapBackground, + }; - public static CommandMapper CommandMapper = new CommandMapper((CommandMapper)(object)ViewHandler.ViewCommandMapper); + public static CommandMapper CommandMapper = + new(ViewHandler.ViewCommandMapper) + { + }; - public DatePickerHandler() - : base((IPropertyMapper)(object)Mapper, (CommandMapper)(object)CommandMapper) - { - } + public DatePickerHandler() : base(Mapper, CommandMapper) + { + } - public DatePickerHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null) - : base((IPropertyMapper)(((object)mapper) ?? ((object)Mapper)), (CommandMapper)(((object)commandMapper) ?? ((object)CommandMapper))) - { - } + public DatePickerHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null) + : base(mapper ?? Mapper, commandMapper ?? CommandMapper) + { + } - protected override SkiaDatePicker CreatePlatformView() - { - return new SkiaDatePicker(); - } + protected override SkiaDatePicker CreatePlatformView() + { + return new SkiaDatePicker(); + } - protected override void ConnectHandler(SkiaDatePicker platformView) - { - //IL_0025: Unknown result type (might be due to invalid IL or missing references) - //IL_002b: Invalid comparison between Unknown and I4 - //IL_0036: Unknown result type (might be due to invalid IL or missing references) - //IL_0050: Unknown result type (might be due to invalid IL or missing references) - //IL_0061: Unknown result type (might be due to invalid IL or missing references) - //IL_0072: Unknown result type (might be due to invalid IL or missing references) - //IL_0083: Unknown result type (might be due to invalid IL or missing references) - base.ConnectHandler(platformView); - platformView.DateSelected += OnDateSelected; - Application current = Application.Current; - if (current != null && (int)current.UserAppTheme == 2) - { - platformView.CalendarBackgroundColor = new SKColor((byte)30, (byte)30, (byte)30); - platformView.TextColor = new SKColor((byte)224, (byte)224, (byte)224); - platformView.BorderColor = new SKColor((byte)97, (byte)97, (byte)97); - platformView.DisabledDayColor = new SKColor((byte)97, (byte)97, (byte)97); - platformView.BackgroundColor = new SKColor((byte)45, (byte)45, (byte)45); - } - } + protected override void ConnectHandler(SkiaDatePicker platformView) + { + base.ConnectHandler(platformView); + platformView.DateSelected += OnDateSelected; + } - protected override void DisconnectHandler(SkiaDatePicker platformView) - { - platformView.DateSelected -= OnDateSelected; - base.DisconnectHandler(platformView); - } + protected override void DisconnectHandler(SkiaDatePicker platformView) + { + platformView.DateSelected -= OnDateSelected; + base.DisconnectHandler(platformView); + } - private void OnDateSelected(object? sender, EventArgs e) - { - if (base.VirtualView != null && base.PlatformView != null) - { - base.VirtualView.Date = base.PlatformView.Date; - } - } + private void OnDateSelected(object? sender, EventArgs e) + { + if (VirtualView is null || PlatformView is null) return; - public static void MapDate(DatePickerHandler handler, IDatePicker datePicker) - { - if (((ViewHandler)(object)handler).PlatformView != null) - { - ((ViewHandler)(object)handler).PlatformView.Date = datePicker.Date; - } - } + VirtualView.Date = PlatformView.Date; + } - public static void MapMinimumDate(DatePickerHandler handler, IDatePicker datePicker) - { - if (((ViewHandler)(object)handler).PlatformView != null) - { - ((ViewHandler)(object)handler).PlatformView.MinimumDate = datePicker.MinimumDate; - } - } + public static void MapDate(DatePickerHandler handler, IDatePicker datePicker) + { + if (handler.PlatformView is null) return; + handler.PlatformView.Date = datePicker.Date; + } - public static void MapMaximumDate(DatePickerHandler handler, IDatePicker datePicker) - { - if (((ViewHandler)(object)handler).PlatformView != null) - { - ((ViewHandler)(object)handler).PlatformView.MaximumDate = datePicker.MaximumDate; - } - } + public static void MapMinimumDate(DatePickerHandler handler, IDatePicker datePicker) + { + if (handler.PlatformView is null) return; + handler.PlatformView.MinimumDate = datePicker.MinimumDate; + } - public static void MapFormat(DatePickerHandler handler, IDatePicker datePicker) - { - if (((ViewHandler)(object)handler).PlatformView != null) - { - ((ViewHandler)(object)handler).PlatformView.Format = datePicker.Format ?? "d"; - } - } + public static void MapMaximumDate(DatePickerHandler handler, IDatePicker datePicker) + { + if (handler.PlatformView is null) return; + handler.PlatformView.MaximumDate = datePicker.MaximumDate; + } - public static void MapTextColor(DatePickerHandler handler, IDatePicker datePicker) - { - //IL_001d: Unknown result type (might be due to invalid IL or missing references) - if (((ViewHandler)(object)handler).PlatformView != null && ((ITextStyle)datePicker).TextColor != null) - { - ((ViewHandler)(object)handler).PlatformView.TextColor = ((ITextStyle)datePicker).TextColor.ToSKColor(); - } - } + public static void MapFormat(DatePickerHandler handler, IDatePicker datePicker) + { + if (handler.PlatformView is null) return; + handler.PlatformView.Format = datePicker.Format ?? "d"; + } - public static void MapCharacterSpacing(DatePickerHandler handler, IDatePicker datePicker) - { - } + public static void MapTextColor(DatePickerHandler handler, IDatePicker datePicker) + { + if (handler.PlatformView is null) return; + if (datePicker.TextColor is not null) + { + handler.PlatformView.TextColor = datePicker.TextColor.ToSKColor(); + } + } - public static void MapBackground(DatePickerHandler handler, IDatePicker datePicker) - { - //IL_002c: Unknown result type (might be due to invalid IL or missing references) - if (((ViewHandler)(object)handler).PlatformView != null) - { - Paint background = ((IView)datePicker).Background; - SolidPaint val = (SolidPaint)(object)((background is SolidPaint) ? background : null); - if (val != null && val.Color != null) - { - ((ViewHandler)(object)handler).PlatformView.BackgroundColor = val.Color.ToSKColor(); - } - } - } + public static void MapCharacterSpacing(DatePickerHandler handler, IDatePicker datePicker) + { + // Character spacing would require custom text rendering + } + + public static void MapBackground(DatePickerHandler handler, IDatePicker datePicker) + { + if (handler.PlatformView is null) return; + + if (datePicker.Background is SolidPaint solidPaint && solidPaint.Color is not null) + { + handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor(); + } + } } diff --git a/Handlers/EditorHandler.cs b/Handlers/EditorHandler.cs index 2d6a0c1..447eede 100644 --- a/Handlers/EditorHandler.cs +++ b/Handlers/EditorHandler.cs @@ -1,182 +1,181 @@ -using System; -using Microsoft.Maui.Controls; -using Microsoft.Maui.Graphics; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using Microsoft.Maui.Handlers; +using Microsoft.Maui.Graphics; +using Microsoft.Maui.Controls; +using Microsoft.Maui.Platform; +using SkiaSharp; namespace Microsoft.Maui.Platform.Linux.Handlers; -public class EditorHandler : ViewHandler +/// +/// Handler for Editor (multiline text) on Linux using Skia rendering. +/// +public partial class EditorHandler : ViewHandler { - public static IPropertyMapper Mapper = (IPropertyMapper)(object)new PropertyMapper((IPropertyMapper[])(object)new IPropertyMapper[1] { (IPropertyMapper)ViewHandler.ViewMapper }) - { - ["Text"] = MapText, - ["Placeholder"] = MapPlaceholder, - ["PlaceholderColor"] = MapPlaceholderColor, - ["TextColor"] = MapTextColor, - ["CharacterSpacing"] = MapCharacterSpacing, - ["IsReadOnly"] = MapIsReadOnly, - ["IsTextPredictionEnabled"] = MapIsTextPredictionEnabled, - ["MaxLength"] = MapMaxLength, - ["CursorPosition"] = MapCursorPosition, - ["SelectionLength"] = MapSelectionLength, - ["Keyboard"] = MapKeyboard, - ["HorizontalTextAlignment"] = MapHorizontalTextAlignment, - ["VerticalTextAlignment"] = MapVerticalTextAlignment, - ["Background"] = MapBackground, - ["BackgroundColor"] = MapBackgroundColor - }; + public static IPropertyMapper Mapper = + new PropertyMapper(ViewHandler.ViewMapper) + { + [nameof(IEditor.Text)] = MapText, + [nameof(IEditor.Placeholder)] = MapPlaceholder, + [nameof(IEditor.PlaceholderColor)] = MapPlaceholderColor, + [nameof(IEditor.TextColor)] = MapTextColor, + [nameof(IEditor.CharacterSpacing)] = MapCharacterSpacing, + [nameof(IEditor.IsReadOnly)] = MapIsReadOnly, + [nameof(IEditor.IsTextPredictionEnabled)] = MapIsTextPredictionEnabled, + [nameof(IEditor.MaxLength)] = MapMaxLength, + [nameof(IEditor.CursorPosition)] = MapCursorPosition, + [nameof(IEditor.SelectionLength)] = MapSelectionLength, + [nameof(IEditor.Keyboard)] = MapKeyboard, + [nameof(IEditor.HorizontalTextAlignment)] = MapHorizontalTextAlignment, + [nameof(IEditor.VerticalTextAlignment)] = MapVerticalTextAlignment, + [nameof(IView.Background)] = MapBackground, + ["BackgroundColor"] = MapBackgroundColor, + }; - public static CommandMapper CommandMapper = new CommandMapper((CommandMapper)(object)ViewHandler.ViewCommandMapper); + public static CommandMapper CommandMapper = + new(ViewHandler.ViewCommandMapper) + { + }; - public EditorHandler() - : base((IPropertyMapper)(object)Mapper, (CommandMapper)(object)CommandMapper) - { - } + public EditorHandler() : base(Mapper, CommandMapper) + { + } - public EditorHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null) - : base((IPropertyMapper)(((object)mapper) ?? ((object)Mapper)), (CommandMapper)(((object)commandMapper) ?? ((object)CommandMapper))) - { - } + public EditorHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null) + : base(mapper ?? Mapper, commandMapper ?? CommandMapper) + { + } - protected override SkiaEditor CreatePlatformView() - { - return new SkiaEditor(); - } + protected override SkiaEditor CreatePlatformView() + { + return new SkiaEditor(); + } - protected override void ConnectHandler(SkiaEditor platformView) - { - base.ConnectHandler(platformView); - platformView.TextChanged += OnTextChanged; - platformView.Completed += OnCompleted; - } + protected override void ConnectHandler(SkiaEditor platformView) + { + base.ConnectHandler(platformView); + platformView.TextChanged += OnTextChanged; + platformView.Completed += OnCompleted; + } - protected override void DisconnectHandler(SkiaEditor platformView) - { - platformView.TextChanged -= OnTextChanged; - platformView.Completed -= OnCompleted; - base.DisconnectHandler(platformView); - } + protected override void DisconnectHandler(SkiaEditor platformView) + { + platformView.TextChanged -= OnTextChanged; + platformView.Completed -= OnCompleted; + base.DisconnectHandler(platformView); + } - private void OnTextChanged(object? sender, EventArgs e) - { - if (base.VirtualView != null && base.PlatformView != null) - { - ((ITextInput)base.VirtualView).Text = base.PlatformView.Text; - } - } + private void OnTextChanged(object? sender, EventArgs e) + { + if (VirtualView is null || PlatformView is null) return; - private void OnCompleted(object? sender, EventArgs e) - { - } + VirtualView.Text = PlatformView.Text; + } - public static void MapText(EditorHandler handler, IEditor editor) - { - if (((ViewHandler)(object)handler).PlatformView != null) - { - ((ViewHandler)(object)handler).PlatformView.Text = ((ITextInput)editor).Text ?? ""; - ((ViewHandler)(object)handler).PlatformView.Invalidate(); - } - } + private void OnCompleted(object? sender, EventArgs e) + { + // Editor doesn't typically have a completed event, but we could trigger it + } - public static void MapPlaceholder(EditorHandler handler, IEditor editor) - { - if (((ViewHandler)(object)handler).PlatformView != null) - { - ((ViewHandler)(object)handler).PlatformView.Placeholder = ((IPlaceholder)editor).Placeholder ?? ""; - } - } + public static void MapText(EditorHandler handler, IEditor editor) + { + if (handler.PlatformView is null) return; + handler.PlatformView.Text = editor.Text ?? ""; + handler.PlatformView.Invalidate(); + } - public static void MapPlaceholderColor(EditorHandler handler, IEditor editor) - { - //IL_001d: Unknown result type (might be due to invalid IL or missing references) - if (((ViewHandler)(object)handler).PlatformView != null && ((IPlaceholder)editor).PlaceholderColor != null) - { - ((ViewHandler)(object)handler).PlatformView.PlaceholderColor = ((IPlaceholder)editor).PlaceholderColor.ToSKColor(); - } - } + public static void MapPlaceholder(EditorHandler handler, IEditor editor) + { + if (handler.PlatformView is null) return; + handler.PlatformView.Placeholder = editor.Placeholder ?? ""; + } - public static void MapTextColor(EditorHandler handler, IEditor editor) - { - //IL_001d: Unknown result type (might be due to invalid IL or missing references) - if (((ViewHandler)(object)handler).PlatformView != null && ((ITextStyle)editor).TextColor != null) - { - ((ViewHandler)(object)handler).PlatformView.TextColor = ((ITextStyle)editor).TextColor.ToSKColor(); - } - } + public static void MapPlaceholderColor(EditorHandler handler, IEditor editor) + { + if (handler.PlatformView is null) return; + if (editor.PlaceholderColor is not null) + { + handler.PlatformView.PlaceholderColor = editor.PlaceholderColor.ToSKColor(); + } + } - public static void MapCharacterSpacing(EditorHandler handler, IEditor editor) - { - } + public static void MapTextColor(EditorHandler handler, IEditor editor) + { + if (handler.PlatformView is null) return; + if (editor.TextColor is not null) + { + handler.PlatformView.TextColor = editor.TextColor.ToSKColor(); + } + } - public static void MapIsReadOnly(EditorHandler handler, IEditor editor) - { - if (((ViewHandler)(object)handler).PlatformView != null) - { - ((ViewHandler)(object)handler).PlatformView.IsReadOnly = ((ITextInput)editor).IsReadOnly; - } - } + public static void MapCharacterSpacing(EditorHandler handler, IEditor editor) + { + // Character spacing would require custom text rendering + } - public static void MapIsTextPredictionEnabled(EditorHandler handler, IEditor editor) - { - } + public static void MapIsReadOnly(EditorHandler handler, IEditor editor) + { + if (handler.PlatformView is null) return; + handler.PlatformView.IsReadOnly = editor.IsReadOnly; + } - public static void MapMaxLength(EditorHandler handler, IEditor editor) - { - if (((ViewHandler)(object)handler).PlatformView != null) - { - ((ViewHandler)(object)handler).PlatformView.MaxLength = ((ITextInput)editor).MaxLength; - } - } + public static void MapIsTextPredictionEnabled(EditorHandler handler, IEditor editor) + { + // Text prediction not applicable to desktop + } - public static void MapCursorPosition(EditorHandler handler, IEditor editor) - { - if (((ViewHandler)(object)handler).PlatformView != null) - { - ((ViewHandler)(object)handler).PlatformView.CursorPosition = ((ITextInput)editor).CursorPosition; - } - } + public static void MapMaxLength(EditorHandler handler, IEditor editor) + { + if (handler.PlatformView is null) return; + handler.PlatformView.MaxLength = editor.MaxLength; + } - public static void MapSelectionLength(EditorHandler handler, IEditor editor) - { - } + public static void MapCursorPosition(EditorHandler handler, IEditor editor) + { + if (handler.PlatformView is null) return; + handler.PlatformView.CursorPosition = editor.CursorPosition; + } - public static void MapKeyboard(EditorHandler handler, IEditor editor) - { - } + public static void MapSelectionLength(EditorHandler handler, IEditor editor) + { + // Selection would need to be added to SkiaEditor + } - public static void MapHorizontalTextAlignment(EditorHandler handler, IEditor editor) - { - } + public static void MapKeyboard(EditorHandler handler, IEditor editor) + { + // Virtual keyboard type not applicable to desktop + } - public static void MapVerticalTextAlignment(EditorHandler handler, IEditor editor) - { - } + public static void MapHorizontalTextAlignment(EditorHandler handler, IEditor editor) + { + // Text alignment would require changes to SkiaEditor drawing + } - public static void MapBackground(EditorHandler handler, IEditor editor) - { - //IL_002c: Unknown result type (might be due to invalid IL or missing references) - if (((ViewHandler)(object)handler).PlatformView != null) - { - Paint background = ((IView)editor).Background; - SolidPaint val = (SolidPaint)(object)((background is SolidPaint) ? background : null); - if (val != null && val.Color != null) - { - ((ViewHandler)(object)handler).PlatformView.BackgroundColor = val.Color.ToSKColor(); - } - } - } + public static void MapVerticalTextAlignment(EditorHandler handler, IEditor editor) + { + // Text alignment would require changes to SkiaEditor drawing + } - public static void MapBackgroundColor(EditorHandler handler, IEditor editor) - { - //IL_0027: Unknown result type (might be due to invalid IL or missing references) - if (((ViewHandler)(object)handler).PlatformView != null) - { - VisualElement val = (VisualElement)(object)((editor is VisualElement) ? editor : null); - if (val != null && val.BackgroundColor != null) - { - ((ViewHandler)(object)handler).PlatformView.BackgroundColor = val.BackgroundColor.ToSKColor(); - ((ViewHandler)(object)handler).PlatformView.Invalidate(); - } - } - } + public static void MapBackground(EditorHandler handler, IEditor editor) + { + if (handler.PlatformView is null) return; + + if (editor.Background is SolidPaint solidPaint && solidPaint.Color is not null) + { + handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor(); + } + } + + public static void MapBackgroundColor(EditorHandler handler, IEditor editor) + { + if (handler.PlatformView is null) return; + + if (editor is VisualElement ve && ve.BackgroundColor != null) + { + handler.PlatformView.BackgroundColor = ve.BackgroundColor.ToSKColor(); + handler.PlatformView.Invalidate(); + } + } } diff --git a/Handlers/EntryHandler.cs b/Handlers/EntryHandler.cs index 1fe9f4b..ac0ff82 100644 --- a/Handlers/EntryHandler.cs +++ b/Handlers/EntryHandler.cs @@ -1,280 +1,215 @@ -using System; -using Microsoft.Maui.Controls; -using Microsoft.Maui.Graphics; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using Microsoft.Maui.Handlers; +using Microsoft.Maui.Graphics; using SkiaSharp; namespace Microsoft.Maui.Platform.Linux.Handlers; -public class EntryHandler : ViewHandler +/// +/// Handler for Entry on Linux using Skia rendering. +/// Maps IEntry interface to SkiaEntry platform view. +/// +public partial class EntryHandler : ViewHandler { - public static IPropertyMapper Mapper = (IPropertyMapper)(object)new PropertyMapper((IPropertyMapper[])(object)new IPropertyMapper[1] { (IPropertyMapper)ViewHandler.ViewMapper }) - { - ["Text"] = MapText, - ["TextColor"] = MapTextColor, - ["Font"] = MapFont, - ["CharacterSpacing"] = MapCharacterSpacing, - ["Placeholder"] = MapPlaceholder, - ["PlaceholderColor"] = MapPlaceholderColor, - ["IsReadOnly"] = MapIsReadOnly, - ["MaxLength"] = MapMaxLength, - ["CursorPosition"] = MapCursorPosition, - ["SelectionLength"] = MapSelectionLength, - ["IsPassword"] = MapIsPassword, - ["ReturnType"] = MapReturnType, - ["ClearButtonVisibility"] = MapClearButtonVisibility, - ["HorizontalTextAlignment"] = MapHorizontalTextAlignment, - ["VerticalTextAlignment"] = MapVerticalTextAlignment, - ["Background"] = MapBackground, - ["BackgroundColor"] = MapBackgroundColor - }; + public static IPropertyMapper Mapper = new PropertyMapper(ViewHandler.ViewMapper) + { + [nameof(ITextInput.Text)] = MapText, + [nameof(ITextStyle.TextColor)] = MapTextColor, + [nameof(ITextStyle.Font)] = MapFont, + [nameof(ITextStyle.CharacterSpacing)] = MapCharacterSpacing, + [nameof(IPlaceholder.Placeholder)] = MapPlaceholder, + [nameof(IPlaceholder.PlaceholderColor)] = MapPlaceholderColor, + [nameof(ITextInput.IsReadOnly)] = MapIsReadOnly, + [nameof(ITextInput.MaxLength)] = MapMaxLength, + [nameof(ITextInput.CursorPosition)] = MapCursorPosition, + [nameof(ITextInput.SelectionLength)] = MapSelectionLength, + [nameof(IEntry.IsPassword)] = MapIsPassword, + [nameof(IEntry.ReturnType)] = MapReturnType, + [nameof(IEntry.ClearButtonVisibility)] = MapClearButtonVisibility, + [nameof(ITextAlignment.HorizontalTextAlignment)] = MapHorizontalTextAlignment, + [nameof(ITextAlignment.VerticalTextAlignment)] = MapVerticalTextAlignment, + [nameof(IView.Background)] = MapBackground, + }; - public static CommandMapper CommandMapper = new CommandMapper((CommandMapper)(object)ViewHandler.ViewCommandMapper); + public static CommandMapper CommandMapper = new(ViewHandler.ViewCommandMapper) + { + }; - public EntryHandler() - : base((IPropertyMapper)(object)Mapper, (CommandMapper)(object)CommandMapper) - { - } + public EntryHandler() : base(Mapper, CommandMapper) + { + } - public EntryHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null) - : base((IPropertyMapper)(((object)mapper) ?? ((object)Mapper)), (CommandMapper)(((object)commandMapper) ?? ((object)CommandMapper))) - { - } + public EntryHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null) + : base(mapper ?? Mapper, commandMapper ?? CommandMapper) + { + } - protected override SkiaEntry CreatePlatformView() - { - return new SkiaEntry(); - } + protected override SkiaEntry CreatePlatformView() + { + return new SkiaEntry(); + } - protected override void ConnectHandler(SkiaEntry platformView) - { - base.ConnectHandler(platformView); - platformView.TextChanged += OnTextChanged; - platformView.Completed += OnCompleted; - } + protected override void ConnectHandler(SkiaEntry platformView) + { + base.ConnectHandler(platformView); + platformView.TextChanged += OnTextChanged; + platformView.Completed += OnCompleted; + } - protected override void DisconnectHandler(SkiaEntry platformView) - { - platformView.TextChanged -= OnTextChanged; - platformView.Completed -= OnCompleted; - base.DisconnectHandler(platformView); - } + protected override void DisconnectHandler(SkiaEntry platformView) + { + platformView.TextChanged -= OnTextChanged; + platformView.Completed -= OnCompleted; + base.DisconnectHandler(platformView); + } - private void OnTextChanged(object? sender, TextChangedEventArgs e) - { - if (base.VirtualView != null && base.PlatformView != null && ((ITextInput)base.VirtualView).Text != e.NewTextValue) - { - ((ITextInput)base.VirtualView).Text = e.NewTextValue ?? string.Empty; - } - } + private void OnTextChanged(object? sender, Platform.TextChangedEventArgs e) + { + if (VirtualView is null || PlatformView is null) return; - private void OnCompleted(object? sender, EventArgs e) - { - IEntry virtualView = base.VirtualView; - if (virtualView != null) - { - virtualView.Completed(); - } - } + if (VirtualView.Text != e.NewTextValue) + { + VirtualView.Text = e.NewTextValue ?? string.Empty; + } + } - public static void MapText(EntryHandler handler, IEntry entry) - { - if (((ViewHandler)(object)handler).PlatformView != null && ((ViewHandler)(object)handler).PlatformView.Text != ((ITextInput)entry).Text) - { - ((ViewHandler)(object)handler).PlatformView.Text = ((ITextInput)entry).Text ?? string.Empty; - ((ViewHandler)(object)handler).PlatformView.Invalidate(); - } - } + private void OnCompleted(object? sender, EventArgs e) + { + VirtualView?.Completed(); + } - public static void MapTextColor(EntryHandler handler, IEntry entry) - { - //IL_001d: Unknown result type (might be due to invalid IL or missing references) - if (((ViewHandler)(object)handler).PlatformView != null && ((ITextStyle)entry).TextColor != null) - { - ((ViewHandler)(object)handler).PlatformView.TextColor = ((ITextStyle)entry).TextColor.ToSKColor(); - } - } + public static void MapText(EntryHandler handler, IEntry entry) + { + if (handler.PlatformView is null) return; - public static void MapFont(EntryHandler handler, IEntry entry) - { - //IL_000a: Unknown result type (might be due to invalid IL or missing references) - //IL_000f: Unknown result type (might be due to invalid IL or missing references) - //IL_005d: Unknown result type (might be due to invalid IL or missing references) - //IL_0067: Invalid comparison between Unknown and I4 - //IL_0079: Unknown result type (might be due to invalid IL or missing references) - //IL_007f: Invalid comparison between Unknown and I4 - //IL_0083: Unknown result type (might be due to invalid IL or missing references) - //IL_0089: Invalid comparison between Unknown and I4 - if (((ViewHandler)(object)handler).PlatformView != null) - { - Font font = ((ITextStyle)entry).Font; - if (((Font)(ref font)).Size > 0.0) - { - ((ViewHandler)(object)handler).PlatformView.FontSize = (float)((Font)(ref font)).Size; - } - if (!string.IsNullOrEmpty(((Font)(ref font)).Family)) - { - ((ViewHandler)(object)handler).PlatformView.FontFamily = ((Font)(ref font)).Family; - } - ((ViewHandler)(object)handler).PlatformView.IsBold = (int)((Font)(ref font)).Weight >= 700; - ((ViewHandler)(object)handler).PlatformView.IsItalic = (int)((Font)(ref font)).Slant == 1 || (int)((Font)(ref font)).Slant == 2; - } - } + if (handler.PlatformView.Text != entry.Text) + { + handler.PlatformView.Text = entry.Text ?? string.Empty; + handler.PlatformView.Invalidate(); + } + } - public static void MapCharacterSpacing(EntryHandler handler, IEntry entry) - { - if (((ViewHandler)(object)handler).PlatformView != null) - { - ((ViewHandler)(object)handler).PlatformView.CharacterSpacing = (float)((ITextStyle)entry).CharacterSpacing; - } - } + public static void MapTextColor(EntryHandler handler, IEntry entry) + { + if (handler.PlatformView is null) return; - public static void MapPlaceholder(EntryHandler handler, IEntry entry) - { - if (((ViewHandler)(object)handler).PlatformView != null) - { - ((ViewHandler)(object)handler).PlatformView.Placeholder = ((IPlaceholder)entry).Placeholder ?? string.Empty; - } - } + if (entry.TextColor is not null) + handler.PlatformView.TextColor = entry.TextColor.ToSKColor(); + } - public static void MapPlaceholderColor(EntryHandler handler, IEntry entry) - { - //IL_001d: Unknown result type (might be due to invalid IL or missing references) - if (((ViewHandler)(object)handler).PlatformView != null && ((IPlaceholder)entry).PlaceholderColor != null) - { - ((ViewHandler)(object)handler).PlatformView.PlaceholderColor = ((IPlaceholder)entry).PlaceholderColor.ToSKColor(); - } - } + public static void MapFont(EntryHandler handler, IEntry entry) + { + if (handler.PlatformView is null) return; - public static void MapIsReadOnly(EntryHandler handler, IEntry entry) - { - if (((ViewHandler)(object)handler).PlatformView != null) - { - ((ViewHandler)(object)handler).PlatformView.IsReadOnly = ((ITextInput)entry).IsReadOnly; - } - } + var font = entry.Font; + if (font.Size > 0) + handler.PlatformView.FontSize = (float)font.Size; - public static void MapMaxLength(EntryHandler handler, IEntry entry) - { - if (((ViewHandler)(object)handler).PlatformView != null) - { - ((ViewHandler)(object)handler).PlatformView.MaxLength = ((ITextInput)entry).MaxLength; - } - } + if (!string.IsNullOrEmpty(font.Family)) + handler.PlatformView.FontFamily = font.Family; - public static void MapCursorPosition(EntryHandler handler, IEntry entry) - { - if (((ViewHandler)(object)handler).PlatformView != null) - { - ((ViewHandler)(object)handler).PlatformView.CursorPosition = ((ITextInput)entry).CursorPosition; - } - } + handler.PlatformView.IsBold = font.Weight >= FontWeight.Bold; + handler.PlatformView.IsItalic = font.Slant == FontSlant.Italic || font.Slant == FontSlant.Oblique; + } - public static void MapSelectionLength(EntryHandler handler, IEntry entry) - { - if (((ViewHandler)(object)handler).PlatformView != null) - { - ((ViewHandler)(object)handler).PlatformView.SelectionLength = ((ITextInput)entry).SelectionLength; - } - } + public static void MapCharacterSpacing(EntryHandler handler, IEntry entry) + { + if (handler.PlatformView is null) return; + handler.PlatformView.CharacterSpacing = (float)entry.CharacterSpacing; + } - public static void MapIsPassword(EntryHandler handler, IEntry entry) - { - if (((ViewHandler)(object)handler).PlatformView != null) - { - ((ViewHandler)(object)handler).PlatformView.IsPassword = entry.IsPassword; - } - } + public static void MapPlaceholder(EntryHandler handler, IEntry entry) + { + if (handler.PlatformView is null) return; + handler.PlatformView.Placeholder = entry.Placeholder ?? string.Empty; + } - public static void MapReturnType(EntryHandler handler, IEntry entry) - { - _ = ((ViewHandler)(object)handler).PlatformView; - } + public static void MapPlaceholderColor(EntryHandler handler, IEntry entry) + { + if (handler.PlatformView is null) return; - public static void MapClearButtonVisibility(EntryHandler handler, IEntry entry) - { - //IL_0010: Unknown result type (might be due to invalid IL or missing references) - //IL_0016: Invalid comparison between Unknown and I4 - if (((ViewHandler)(object)handler).PlatformView != null) - { - ((ViewHandler)(object)handler).PlatformView.ShowClearButton = (int)entry.ClearButtonVisibility == 1; - } - } + if (entry.PlaceholderColor is not null) + handler.PlatformView.PlaceholderColor = entry.PlaceholderColor.ToSKColor(); + } - public static void MapHorizontalTextAlignment(EntryHandler handler, IEntry entry) - { - //IL_0011: Unknown result type (might be due to invalid IL or missing references) - //IL_0016: Unknown result type (might be due to invalid IL or missing references) - //IL_0017: Unknown result type (might be due to invalid IL or missing references) - //IL_0029: Expected I4, but got Unknown - if (((ViewHandler)(object)handler).PlatformView != null) - { - SkiaEntry platformView = ((ViewHandler)(object)handler).PlatformView; - TextAlignment horizontalTextAlignment = ((ITextAlignment)entry).HorizontalTextAlignment; - platformView.HorizontalTextAlignment = (int)horizontalTextAlignment switch - { - 0 => TextAlignment.Start, - 1 => TextAlignment.Center, - 2 => TextAlignment.End, - _ => TextAlignment.Start, - }; - } - } + public static void MapIsReadOnly(EntryHandler handler, IEntry entry) + { + if (handler.PlatformView is null) return; + handler.PlatformView.IsReadOnly = entry.IsReadOnly; + } - public static void MapVerticalTextAlignment(EntryHandler handler, IEntry entry) - { - //IL_0011: Unknown result type (might be due to invalid IL or missing references) - //IL_0016: Unknown result type (might be due to invalid IL or missing references) - //IL_0017: Unknown result type (might be due to invalid IL or missing references) - //IL_0029: Expected I4, but got Unknown - if (((ViewHandler)(object)handler).PlatformView != null) - { - SkiaEntry platformView = ((ViewHandler)(object)handler).PlatformView; - TextAlignment verticalTextAlignment = ((ITextAlignment)entry).VerticalTextAlignment; - platformView.VerticalTextAlignment = (int)verticalTextAlignment switch - { - 0 => TextAlignment.Start, - 1 => TextAlignment.Center, - 2 => TextAlignment.End, - _ => TextAlignment.Center, - }; - } - } + public static void MapMaxLength(EntryHandler handler, IEntry entry) + { + if (handler.PlatformView is null) return; + handler.PlatformView.MaxLength = entry.MaxLength; + } - public static void MapBackground(EntryHandler handler, IEntry entry) - { - //IL_002c: Unknown result type (might be due to invalid IL or missing references) - if (((ViewHandler)(object)handler).PlatformView != null) - { - Paint background = ((IView)entry).Background; - SolidPaint val = (SolidPaint)(object)((background is SolidPaint) ? background : null); - if (val != null && val.Color != null) - { - ((ViewHandler)(object)handler).PlatformView.BackgroundColor = val.Color.ToSKColor(); - } - } - } + public static void MapCursorPosition(EntryHandler handler, IEntry entry) + { + if (handler.PlatformView is null) return; + handler.PlatformView.CursorPosition = entry.CursorPosition; + } - public static void MapBackgroundColor(EntryHandler handler, IEntry entry) - { - //IL_0050: Unknown result type (might be due to invalid IL or missing references) - //IL_0055: Unknown result type (might be due to invalid IL or missing references) - //IL_006e: Unknown result type (might be due to invalid IL or missing references) - //IL_0086: Unknown result type (might be due to invalid IL or missing references) - if (((ViewHandler)(object)handler).PlatformView == null) - { - return; - } - Entry val = (Entry)(object)((entry is Entry) ? entry : null); - if (val != null) - { - Console.WriteLine($"[EntryHandler] MapBackgroundColor: {((VisualElement)val).BackgroundColor}"); - if (((VisualElement)val).BackgroundColor != null) - { - SKColor val2 = ((VisualElement)val).BackgroundColor.ToSKColor(); - Console.WriteLine($"[EntryHandler] Setting EntryBackgroundColor to: {val2}"); - ((ViewHandler)(object)handler).PlatformView.EntryBackgroundColor = val2; - } - } - } + public static void MapSelectionLength(EntryHandler handler, IEntry entry) + { + if (handler.PlatformView is null) return; + handler.PlatformView.SelectionLength = entry.SelectionLength; + } + + public static void MapIsPassword(EntryHandler handler, IEntry entry) + { + if (handler.PlatformView is null) return; + handler.PlatformView.IsPassword = entry.IsPassword; + } + + public static void MapReturnType(EntryHandler handler, IEntry entry) + { + // ReturnType affects keyboard behavior - stored for virtual keyboard integration + if (handler.PlatformView is null) return; + // handler.PlatformView.ReturnType = entry.ReturnType; // Would need property on SkiaEntry + } + + public static void MapClearButtonVisibility(EntryHandler handler, IEntry entry) + { + if (handler.PlatformView is null) return; + handler.PlatformView.ShowClearButton = entry.ClearButtonVisibility == ClearButtonVisibility.WhileEditing; + } + + public static void MapHorizontalTextAlignment(EntryHandler handler, IEntry entry) + { + if (handler.PlatformView is null) return; + + handler.PlatformView.HorizontalTextAlignment = entry.HorizontalTextAlignment switch + { + Microsoft.Maui.TextAlignment.Start => Platform.TextAlignment.Start, + Microsoft.Maui.TextAlignment.Center => Platform.TextAlignment.Center, + Microsoft.Maui.TextAlignment.End => Platform.TextAlignment.End, + _ => Platform.TextAlignment.Start + }; + } + + public static void MapVerticalTextAlignment(EntryHandler handler, IEntry entry) + { + if (handler.PlatformView is null) return; + + handler.PlatformView.VerticalTextAlignment = entry.VerticalTextAlignment switch + { + Microsoft.Maui.TextAlignment.Start => Platform.TextAlignment.Start, + Microsoft.Maui.TextAlignment.Center => Platform.TextAlignment.Center, + Microsoft.Maui.TextAlignment.End => Platform.TextAlignment.End, + _ => Platform.TextAlignment.Center + }; + } + + public static void MapBackground(EntryHandler handler, IEntry entry) + { + if (handler.PlatformView is null) return; + + if (entry.Background is SolidPaint solidPaint && solidPaint.Color is not null) + { + handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor(); + } + } } diff --git a/Handlers/FlexLayoutHandler.cs b/Handlers/FlexLayoutHandler.cs deleted file mode 100644 index 71f854d..0000000 --- a/Handlers/FlexLayoutHandler.cs +++ /dev/null @@ -1,139 +0,0 @@ -using Microsoft.Maui.Controls; -using Microsoft.Maui.Handlers; -using Microsoft.Maui.Layouts; - -namespace Microsoft.Maui.Platform.Linux.Handlers; - -public class FlexLayoutHandler : LayoutHandler -{ - public new static IPropertyMapper Mapper = (IPropertyMapper)(object)new PropertyMapper((IPropertyMapper[])(object)new IPropertyMapper[1] { (IPropertyMapper)LayoutHandler.Mapper }) - { - ["Direction"] = MapDirection, - ["Wrap"] = MapWrap, - ["JustifyContent"] = MapJustifyContent, - ["AlignItems"] = MapAlignItems, - ["AlignContent"] = MapAlignContent - }; - - public FlexLayoutHandler() - : base((IPropertyMapper?)(object)Mapper) - { - } - - protected override SkiaLayoutView CreatePlatformView() - { - return new SkiaFlexLayout(); - } - - public static void MapDirection(FlexLayoutHandler handler, FlexLayout layout) - { - //IL_0012: Unknown result type (might be due to invalid IL or missing references) - //IL_0017: Unknown result type (might be due to invalid IL or missing references) - //IL_0018: Unknown result type (might be due to invalid IL or missing references) - //IL_002e: Expected I4, but got Unknown - if (((ViewHandler)(object)handler).PlatformView is SkiaFlexLayout skiaFlexLayout) - { - SkiaFlexLayout skiaFlexLayout2 = skiaFlexLayout; - FlexDirection direction = layout.Direction; - skiaFlexLayout2.Direction = (int)direction switch - { - 0 => FlexDirection.Row, - 1 => FlexDirection.RowReverse, - 2 => FlexDirection.Column, - 3 => FlexDirection.ColumnReverse, - _ => FlexDirection.Row, - }; - } - } - - public static void MapWrap(FlexLayoutHandler handler, FlexLayout layout) - { - //IL_0012: Unknown result type (might be due to invalid IL or missing references) - //IL_0017: Unknown result type (might be due to invalid IL or missing references) - //IL_0018: Unknown result type (might be due to invalid IL or missing references) - //IL_002a: Expected I4, but got Unknown - if (((ViewHandler)(object)handler).PlatformView is SkiaFlexLayout skiaFlexLayout) - { - SkiaFlexLayout skiaFlexLayout2 = skiaFlexLayout; - FlexWrap wrap = layout.Wrap; - skiaFlexLayout2.Wrap = (int)wrap switch - { - 0 => FlexWrap.NoWrap, - 1 => FlexWrap.Wrap, - 2 => FlexWrap.WrapReverse, - _ => FlexWrap.NoWrap, - }; - } - } - - public static void MapJustifyContent(FlexLayoutHandler handler, FlexLayout layout) - { - //IL_0012: Unknown result type (might be due to invalid IL or missing references) - //IL_0017: Unknown result type (might be due to invalid IL or missing references) - //IL_0018: Unknown result type (might be due to invalid IL or missing references) - //IL_001a: Unknown result type (might be due to invalid IL or missing references) - //IL_0038: Expected I4, but got Unknown - if (((ViewHandler)(object)handler).PlatformView is SkiaFlexLayout skiaFlexLayout) - { - SkiaFlexLayout skiaFlexLayout2 = skiaFlexLayout; - FlexJustify justifyContent = layout.JustifyContent; - skiaFlexLayout2.JustifyContent = (justifyContent - 2) switch - { - 1 => FlexJustify.Start, - 0 => FlexJustify.Center, - 2 => FlexJustify.End, - 3 => FlexJustify.SpaceBetween, - 4 => FlexJustify.SpaceAround, - 5 => FlexJustify.SpaceEvenly, - _ => FlexJustify.Start, - }; - } - } - - public static void MapAlignItems(FlexLayoutHandler handler, FlexLayout layout) - { - //IL_0012: Unknown result type (might be due to invalid IL or missing references) - //IL_0017: Unknown result type (might be due to invalid IL or missing references) - //IL_0018: Unknown result type (might be due to invalid IL or missing references) - //IL_001a: Unknown result type (might be due to invalid IL or missing references) - //IL_0030: Expected I4, but got Unknown - if (((ViewHandler)(object)handler).PlatformView is SkiaFlexLayout skiaFlexLayout) - { - SkiaFlexLayout skiaFlexLayout2 = skiaFlexLayout; - FlexAlignItems alignItems = layout.AlignItems; - skiaFlexLayout2.AlignItems = (alignItems - 1) switch - { - 2 => FlexAlignItems.Start, - 1 => FlexAlignItems.Center, - 3 => FlexAlignItems.End, - 0 => FlexAlignItems.Stretch, - _ => FlexAlignItems.Stretch, - }; - } - } - - public static void MapAlignContent(FlexLayoutHandler handler, FlexLayout layout) - { - //IL_0012: Unknown result type (might be due to invalid IL or missing references) - //IL_0017: Unknown result type (might be due to invalid IL or missing references) - //IL_0018: Unknown result type (might be due to invalid IL or missing references) - //IL_001a: Unknown result type (might be due to invalid IL or missing references) - //IL_003c: Expected I4, but got Unknown - if (((ViewHandler)(object)handler).PlatformView is SkiaFlexLayout skiaFlexLayout) - { - SkiaFlexLayout skiaFlexLayout2 = skiaFlexLayout; - FlexAlignContent alignContent = layout.AlignContent; - skiaFlexLayout2.AlignContent = (alignContent - 1) switch - { - 2 => FlexAlignContent.Start, - 1 => FlexAlignContent.Center, - 3 => FlexAlignContent.End, - 0 => FlexAlignContent.Stretch, - 4 => FlexAlignContent.SpaceBetween, - 5 => FlexAlignContent.SpaceAround, - 6 => FlexAlignContent.SpaceAround, - _ => FlexAlignContent.Stretch, - }; - } - } -} diff --git a/Handlers/FlyoutPageHandler.cs b/Handlers/FlyoutPageHandler.cs index 29afd15..74f9194 100644 --- a/Handlers/FlyoutPageHandler.cs +++ b/Handlers/FlyoutPageHandler.cs @@ -1,94 +1,91 @@ -using System; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using Microsoft.Maui.Handlers; +using Microsoft.Maui.Graphics; +using SkiaSharp; namespace Microsoft.Maui.Platform.Linux.Handlers; -public class FlyoutPageHandler : ViewHandler +/// +/// Handler for FlyoutPage on Linux using Skia rendering. +/// Maps IFlyoutView interface to SkiaFlyoutPage platform view. +/// +public partial class FlyoutPageHandler : ViewHandler { - public static IPropertyMapper Mapper = (IPropertyMapper)(object)new PropertyMapper((IPropertyMapper[])(object)new IPropertyMapper[1] { (IPropertyMapper)ViewHandler.ViewMapper }) - { - ["IsPresented"] = MapIsPresented, - ["FlyoutWidth"] = MapFlyoutWidth, - ["IsGestureEnabled"] = MapIsGestureEnabled, - ["FlyoutBehavior"] = MapFlyoutBehavior - }; + public static IPropertyMapper Mapper = new PropertyMapper(ViewHandler.ViewMapper) + { + [nameof(IFlyoutView.IsPresented)] = MapIsPresented, + [nameof(IFlyoutView.FlyoutWidth)] = MapFlyoutWidth, + [nameof(IFlyoutView.IsGestureEnabled)] = MapIsGestureEnabled, + [nameof(IFlyoutView.FlyoutBehavior)] = MapFlyoutBehavior, + }; - public static CommandMapper CommandMapper = new CommandMapper((CommandMapper)(object)ViewHandler.ViewCommandMapper); + public static CommandMapper CommandMapper = new(ViewHandler.ViewCommandMapper) + { + }; - public FlyoutPageHandler() - : base((IPropertyMapper)(object)Mapper, (CommandMapper)(object)CommandMapper) - { - } + public FlyoutPageHandler() : base(Mapper, CommandMapper) + { + } - public FlyoutPageHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null) - : base((IPropertyMapper)(((object)mapper) ?? ((object)Mapper)), (CommandMapper)(((object)commandMapper) ?? ((object)CommandMapper))) - { - } + public FlyoutPageHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null) + : base(mapper ?? Mapper, commandMapper ?? CommandMapper) + { + } - protected override SkiaFlyoutPage CreatePlatformView() - { - return new SkiaFlyoutPage(); - } + protected override SkiaFlyoutPage CreatePlatformView() + { + return new SkiaFlyoutPage(); + } - protected override void ConnectHandler(SkiaFlyoutPage platformView) - { - base.ConnectHandler(platformView); - platformView.IsPresentedChanged += OnIsPresentedChanged; - } + protected override void ConnectHandler(SkiaFlyoutPage platformView) + { + base.ConnectHandler(platformView); + platformView.IsPresentedChanged += OnIsPresentedChanged; + } - protected override void DisconnectHandler(SkiaFlyoutPage platformView) - { - platformView.IsPresentedChanged -= OnIsPresentedChanged; - platformView.Flyout = null; - platformView.Detail = null; - base.DisconnectHandler(platformView); - } + protected override void DisconnectHandler(SkiaFlyoutPage platformView) + { + platformView.IsPresentedChanged -= OnIsPresentedChanged; + platformView.Flyout = null; + platformView.Detail = null; + base.DisconnectHandler(platformView); + } - private void OnIsPresentedChanged(object? sender, EventArgs e) - { - } + private void OnIsPresentedChanged(object? sender, EventArgs e) + { + // Sync back to the virtual view + } - public static void MapIsPresented(FlyoutPageHandler handler, IFlyoutView flyoutView) - { - if (((ViewHandler)(object)handler).PlatformView != null) - { - ((ViewHandler)(object)handler).PlatformView.IsPresented = flyoutView.IsPresented; - } - } + public static void MapIsPresented(FlyoutPageHandler handler, IFlyoutView flyoutView) + { + if (handler.PlatformView is null) return; + handler.PlatformView.IsPresented = flyoutView.IsPresented; + } - public static void MapFlyoutWidth(FlyoutPageHandler handler, IFlyoutView flyoutView) - { - if (((ViewHandler)(object)handler).PlatformView != null) - { - ((ViewHandler)(object)handler).PlatformView.FlyoutWidth = (float)flyoutView.FlyoutWidth; - } - } + public static void MapFlyoutWidth(FlyoutPageHandler handler, IFlyoutView flyoutView) + { + if (handler.PlatformView is null) return; + handler.PlatformView.FlyoutWidth = (float)flyoutView.FlyoutWidth; + } - public static void MapIsGestureEnabled(FlyoutPageHandler handler, IFlyoutView flyoutView) - { - if (((ViewHandler)(object)handler).PlatformView != null) - { - ((ViewHandler)(object)handler).PlatformView.GestureEnabled = flyoutView.IsGestureEnabled; - } - } + public static void MapIsGestureEnabled(FlyoutPageHandler handler, IFlyoutView flyoutView) + { + if (handler.PlatformView is null) return; + handler.PlatformView.GestureEnabled = flyoutView.IsGestureEnabled; + } - public static void MapFlyoutBehavior(FlyoutPageHandler handler, IFlyoutView flyoutView) - { - //IL_0011: Unknown result type (might be due to invalid IL or missing references) - //IL_0016: Unknown result type (might be due to invalid IL or missing references) - //IL_0017: Unknown result type (might be due to invalid IL or missing references) - //IL_0029: Expected I4, but got Unknown - if (((ViewHandler)(object)handler).PlatformView != null) - { - SkiaFlyoutPage platformView = ((ViewHandler)(object)handler).PlatformView; - FlyoutBehavior flyoutBehavior = flyoutView.FlyoutBehavior; - platformView.FlyoutLayoutBehavior = (int)flyoutBehavior switch - { - 0 => FlyoutLayoutBehavior.Default, - 1 => FlyoutLayoutBehavior.Popover, - 2 => FlyoutLayoutBehavior.Split, - _ => FlyoutLayoutBehavior.Default, - }; - } - } + public static void MapFlyoutBehavior(FlyoutPageHandler handler, IFlyoutView flyoutView) + { + if (handler.PlatformView is null) return; + + handler.PlatformView.FlyoutLayoutBehavior = flyoutView.FlyoutBehavior switch + { + Microsoft.Maui.FlyoutBehavior.Disabled => FlyoutLayoutBehavior.Default, + Microsoft.Maui.FlyoutBehavior.Flyout => FlyoutLayoutBehavior.Popover, + Microsoft.Maui.FlyoutBehavior.Locked => FlyoutLayoutBehavior.Split, + _ => FlyoutLayoutBehavior.Default + }; + } } diff --git a/Handlers/FrameHandler.cs b/Handlers/FrameHandler.cs index ecbf8d2..88847b7 100644 --- a/Handlers/FrameHandler.cs +++ b/Handlers/FrameHandler.cs @@ -1,134 +1,104 @@ -using System; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using Microsoft.Maui.Controls; -using Microsoft.Maui.Controls.Compatibility; using Microsoft.Maui.Handlers; -using Microsoft.Maui.Platform.Linux.Hosting; using SkiaSharp; namespace Microsoft.Maui.Platform.Linux.Handlers; -public class FrameHandler : ViewHandler +/// +/// Handler for Frame on Linux using SkiaFrame. +/// +public partial class FrameHandler : ViewHandler { - public static IPropertyMapper Mapper = (IPropertyMapper)(object)new PropertyMapper((IPropertyMapper[])(object)new IPropertyMapper[1] { (IPropertyMapper)ViewHandler.ViewMapper }) - { - ["BorderColor"] = MapBorderColor, - ["CornerRadius"] = MapCornerRadius, - ["HasShadow"] = MapHasShadow, - ["BackgroundColor"] = MapBackgroundColor, - ["Padding"] = MapPadding, - ["Content"] = MapContent - }; + public static IPropertyMapper Mapper = + new PropertyMapper(ViewMapper) + { + [nameof(Frame.BorderColor)] = MapBorderColor, + [nameof(Frame.CornerRadius)] = MapCornerRadius, + [nameof(Frame.HasShadow)] = MapHasShadow, + [nameof(Frame.BackgroundColor)] = MapBackgroundColor, + [nameof(Frame.Padding)] = MapPadding, + [nameof(Frame.Content)] = MapContent, + }; - public FrameHandler() - : base((IPropertyMapper)(object)Mapper, (CommandMapper)null) - { - } + public FrameHandler() : base(Mapper) + { + } - public FrameHandler(IPropertyMapper? mapper) - : base((IPropertyMapper)(((object)mapper) ?? ((object)Mapper)), (CommandMapper)null) - { - } + public FrameHandler(IPropertyMapper? mapper) + : base(mapper ?? Mapper) + { + } - protected override SkiaFrame CreatePlatformView() - { - return new SkiaFrame(); - } + protected override SkiaFrame CreatePlatformView() + { + return new SkiaFrame(); + } - protected override void ConnectHandler(SkiaFrame platformView) - { - base.ConnectHandler(platformView); - View virtualView = (View)(object)base.VirtualView; - if (virtualView != null) - { - platformView.MauiView = virtualView; - } - platformView.Tapped += OnPlatformViewTapped; - } + public static void MapBorderColor(FrameHandler handler, Frame frame) + { + if (frame.BorderColor != null) + { + handler.PlatformView.Stroke = new SKColor( + (byte)(frame.BorderColor.Red * 255), + (byte)(frame.BorderColor.Green * 255), + (byte)(frame.BorderColor.Blue * 255), + (byte)(frame.BorderColor.Alpha * 255)); + } + } - protected override void DisconnectHandler(SkiaFrame platformView) - { - platformView.Tapped -= OnPlatformViewTapped; - platformView.MauiView = null; - base.DisconnectHandler(platformView); - } + public static void MapCornerRadius(FrameHandler handler, Frame frame) + { + handler.PlatformView.CornerRadius = frame.CornerRadius; + } - private void OnPlatformViewTapped(object? sender, EventArgs e) - { - View virtualView = (View)(object)base.VirtualView; - if (virtualView != null) - { - GestureManager.ProcessTap(virtualView, 0.0, 0.0); - } - } + public static void MapHasShadow(FrameHandler handler, Frame frame) + { + handler.PlatformView.HasShadow = frame.HasShadow; + } - public static void MapBorderColor(FrameHandler handler, Frame frame) - { - //IL_0056: Unknown result type (might be due to invalid IL or missing references) - if (frame.BorderColor != null) - { - ((ViewHandler)(object)handler).PlatformView.Stroke = new SKColor((byte)(frame.BorderColor.Red * 255f), (byte)(frame.BorderColor.Green * 255f), (byte)(frame.BorderColor.Blue * 255f), (byte)(frame.BorderColor.Alpha * 255f)); - } - } + public static void MapBackgroundColor(FrameHandler handler, Frame frame) + { + if (frame.BackgroundColor != null) + { + handler.PlatformView.BackgroundColor = new SKColor( + (byte)(frame.BackgroundColor.Red * 255), + (byte)(frame.BackgroundColor.Green * 255), + (byte)(frame.BackgroundColor.Blue * 255), + (byte)(frame.BackgroundColor.Alpha * 255)); + } + } - public static void MapCornerRadius(FrameHandler handler, Frame frame) - { - ((ViewHandler)(object)handler).PlatformView.CornerRadius = frame.CornerRadius; - } + public static void MapPadding(FrameHandler handler, Frame frame) + { + handler.PlatformView.SetPadding( + (float)frame.Padding.Left, + (float)frame.Padding.Top, + (float)frame.Padding.Right, + (float)frame.Padding.Bottom); + } - public static void MapHasShadow(FrameHandler handler, Frame frame) - { - ((ViewHandler)(object)handler).PlatformView.HasShadow = frame.HasShadow; - } + public static void MapContent(FrameHandler handler, Frame frame) + { + if (handler.PlatformView is null || handler.MauiContext is null) return; - public static void MapBackgroundColor(FrameHandler handler, Frame frame) - { - //IL_0056: Unknown result type (might be due to invalid IL or missing references) - if (((VisualElement)frame).BackgroundColor != null) - { - ((ViewHandler)(object)handler).PlatformView.BackgroundColor = new SKColor((byte)(((VisualElement)frame).BackgroundColor.Red * 255f), (byte)(((VisualElement)frame).BackgroundColor.Green * 255f), (byte)(((VisualElement)frame).BackgroundColor.Blue * 255f), (byte)(((VisualElement)frame).BackgroundColor.Alpha * 255f)); - } - } + handler.PlatformView.ClearChildren(); - public static void MapPadding(FrameHandler handler, Frame frame) - { - //IL_0007: Unknown result type (might be due to invalid IL or missing references) - //IL_000c: Unknown result type (might be due to invalid IL or missing references) - //IL_0016: Unknown result type (might be due to invalid IL or missing references) - //IL_001b: Unknown result type (might be due to invalid IL or missing references) - //IL_0025: Unknown result type (might be due to invalid IL or missing references) - //IL_002a: Unknown result type (might be due to invalid IL or missing references) - //IL_0034: Unknown result type (might be due to invalid IL or missing references) - //IL_0039: Unknown result type (might be due to invalid IL or missing references) - SkiaFrame platformView = ((ViewHandler)(object)handler).PlatformView; - Thickness padding = ((Layout)frame).Padding; - float left = (float)((Thickness)(ref padding)).Left; - padding = ((Layout)frame).Padding; - float top = (float)((Thickness)(ref padding)).Top; - padding = ((Layout)frame).Padding; - float right = (float)((Thickness)(ref padding)).Right; - padding = ((Layout)frame).Padding; - platformView.SetPadding(left, top, right, (float)((Thickness)(ref padding)).Bottom); - } + var content = frame.Content; + if (content != null) + { + // Create handler for content if it doesn't exist + if (content.Handler == null) + { + content.Handler = content.ToHandler(handler.MauiContext); + } - public static void MapContent(FrameHandler handler, Frame frame) - { - if (((ViewHandler)(object)handler).PlatformView == null || ((ElementHandler)handler).MauiContext == null) - { - return; - } - ((ViewHandler)(object)handler).PlatformView.ClearChildren(); - View content = ((ContentView)frame).Content; - if (content != null) - { - if (((VisualElement)content).Handler == null) - { - ((VisualElement)content).Handler = ((IView)(object)content).ToViewHandler(((ElementHandler)handler).MauiContext); - } - IViewHandler handler2 = ((VisualElement)content).Handler; - if (((handler2 != null) ? ((IElementHandler)handler2).PlatformView : null) is SkiaView child) - { - ((ViewHandler)(object)handler).PlatformView.AddChild(child); - } - } - } + if (content.Handler?.PlatformView is SkiaView skiaContent) + { + handler.PlatformView.AddChild(skiaContent); + } + } + } } diff --git a/Handlers/GestureManager.cs b/Handlers/GestureManager.cs deleted file mode 100644 index d346539..0000000 --- a/Handlers/GestureManager.cs +++ /dev/null @@ -1,547 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Windows.Input; -using Microsoft.Maui.Controls; - -namespace Microsoft.Maui.Platform.Linux.Handlers; - -public static class GestureManager -{ - private class GestureTrackingState - { - public double StartX { get; set; } - - public double StartY { get; set; } - - public double CurrentX { get; set; } - - public double CurrentY { get; set; } - - public DateTime StartTime { get; set; } - - public bool IsPanning { get; set; } - - public bool IsPressed { get; set; } - } - - private enum PointerEventType - { - Entered, - Exited, - Pressed, - Moved, - Released - } - - private static MethodInfo? _sendTappedMethod; - - private static readonly Dictionary _tapTracking = new Dictionary(); - - private static readonly Dictionary _gestureState = new Dictionary(); - - private const double SwipeMinDistance = 50.0; - - private const double SwipeMaxTime = 500.0; - - private const double SwipeDirectionThreshold = 0.5; - - private const double PanMinDistance = 10.0; - - public static bool ProcessTap(View? view, double x, double y) - { - if (view == null) - { - return false; - } - View val = view; - while (val != null) - { - IList gestureRecognizers = val.GestureRecognizers; - if (gestureRecognizers != null && gestureRecognizers.Count > 0 && ProcessTapOnView(val, x, y)) - { - return true; - } - Element parent = ((Element)val).Parent; - val = (View)(object)((parent is View) ? parent : null); - } - return false; - } - - private static bool ProcessTapOnView(View view, double x, double y) - { - //IL_031f: Unknown result type (might be due to invalid IL or missing references) - //IL_0326: Expected O, but got Unknown - //IL_03cb: Unknown result type (might be due to invalid IL or missing references) - //IL_03d2: Expected O, but got Unknown - //IL_026e: Unknown result type (might be due to invalid IL or missing references) - //IL_0275: Expected O, but got Unknown - IList gestureRecognizers = view.GestureRecognizers; - if (gestureRecognizers == null || gestureRecognizers.Count == 0) - { - return false; - } - bool result = false; - foreach (IGestureRecognizer item in gestureRecognizers) - { - TapGestureRecognizer val = (TapGestureRecognizer)(object)((item is TapGestureRecognizer) ? item : null); - if (val == null) - { - continue; - } - Console.WriteLine($"[GestureManager] Processing TapGestureRecognizer on {((object)view).GetType().Name}, CommandParameter={val.CommandParameter}, NumberOfTapsRequired={val.NumberOfTapsRequired}"); - int numberOfTapsRequired = val.NumberOfTapsRequired; - if (numberOfTapsRequired > 1) - { - DateTime utcNow = DateTime.UtcNow; - if (!_tapTracking.TryGetValue(view, out (DateTime, int) value)) - { - _tapTracking[view] = (utcNow, 1); - Console.WriteLine($"[GestureManager] First tap 1/{numberOfTapsRequired}"); - continue; - } - if (!((utcNow - value.Item1).TotalMilliseconds < 300.0)) - { - _tapTracking[view] = (utcNow, 1); - Console.WriteLine($"[GestureManager] Tap timeout, reset to 1/{numberOfTapsRequired}"); - continue; - } - int num = value.Item2 + 1; - if (num < numberOfTapsRequired) - { - _tapTracking[view] = (utcNow, num); - Console.WriteLine($"[GestureManager] Tap {num}/{numberOfTapsRequired}, waiting for more taps"); - continue; - } - _tapTracking.Remove(view); - } - bool flag = false; - try - { - if ((object)_sendTappedMethod == null) - { - _sendTappedMethod = typeof(TapGestureRecognizer).GetMethod("SendTapped", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); - } - if (_sendTappedMethod != null) - { - Console.WriteLine($"[GestureManager] Found SendTapped method with {_sendTappedMethod.GetParameters().Length} params"); - TappedEventArgs e = new TappedEventArgs(val.CommandParameter); - _sendTappedMethod.Invoke(val, new object[2] { view, e }); - Console.WriteLine("[GestureManager] SendTapped invoked successfully"); - flag = true; - } - } - catch (Exception ex) - { - Console.WriteLine("[GestureManager] SendTapped failed: " + ex.Message); - } - if (!flag) - { - try - { - FieldInfo fieldInfo = typeof(TapGestureRecognizer).GetField("Tapped", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) ?? typeof(TapGestureRecognizer).GetField("_tapped", BindingFlags.Instance | BindingFlags.NonPublic); - if (fieldInfo != null && fieldInfo.GetValue(val) is EventHandler eventHandler) - { - Console.WriteLine("[GestureManager] Invoking Tapped event directly"); - TappedEventArgs e2 = new TappedEventArgs(val.CommandParameter); - eventHandler(val, e2); - flag = true; - } - } - catch (Exception ex2) - { - Console.WriteLine("[GestureManager] Direct event invoke failed: " + ex2.Message); - } - } - if (!flag) - { - try - { - string[] array = new string[3] { "TappedEvent", "_TappedHandler", "k__BackingField" }; - foreach (string text in array) - { - FieldInfo field = typeof(TapGestureRecognizer).GetField(text, BindingFlags.Instance | BindingFlags.NonPublic); - if (field != null) - { - Console.WriteLine("[GestureManager] Found field: " + text); - if (field.GetValue(val) is EventHandler eventHandler2) - { - TappedEventArgs e3 = new TappedEventArgs(val.CommandParameter); - eventHandler2(val, e3); - Console.WriteLine("[GestureManager] Event fired via " + text); - flag = true; - break; - } - } - } - } - catch (Exception ex3) - { - Console.WriteLine("[GestureManager] Backing field approach failed: " + ex3.Message); - } - } - if (!flag) - { - Console.WriteLine("[GestureManager] Could not fire event, dumping type info..."); - MethodInfo[] methods = typeof(TapGestureRecognizer).GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); - foreach (MethodInfo methodInfo in methods) - { - if (methodInfo.Name.Contains("Tap", StringComparison.OrdinalIgnoreCase) || methodInfo.Name.Contains("Send", StringComparison.OrdinalIgnoreCase)) - { - Console.WriteLine($"[GestureManager] Method: {methodInfo.Name}({string.Join(", ", from p in methodInfo.GetParameters() - select p.ParameterType.Name)})"); - } - } - } - ICommand command = val.Command; - if (command != null && command.CanExecute(val.CommandParameter)) - { - Console.WriteLine("[GestureManager] Executing Command"); - val.Command.Execute(val.CommandParameter); - } - result = true; - } - return result; - } - - public static bool HasGestureRecognizers(View? view) - { - if (view == null) - { - return false; - } - return view.GestureRecognizers?.Count > 0; - } - - public static bool HasTapGestureRecognizer(View? view) - { - if (((view != null) ? view.GestureRecognizers : null) == null) - { - return false; - } - foreach (IGestureRecognizer gestureRecognizer in view.GestureRecognizers) - { - if (gestureRecognizer is TapGestureRecognizer) - { - return true; - } - } - return false; - } - - public static void ProcessPointerDown(View? view, double x, double y) - { - if (view != null) - { - _gestureState[view] = new GestureTrackingState - { - StartX = x, - StartY = y, - CurrentX = x, - CurrentY = y, - StartTime = DateTime.UtcNow, - IsPanning = false, - IsPressed = true - }; - ProcessPointerEvent(view, x, y, PointerEventType.Pressed); - } - } - - public static void ProcessPointerMove(View? view, double x, double y) - { - if (view == null) - { - return; - } - if (!_gestureState.TryGetValue(view, out GestureTrackingState value)) - { - ProcessPointerEvent(view, x, y, PointerEventType.Moved); - return; - } - value.CurrentX = x; - value.CurrentY = y; - if (!value.IsPressed) - { - ProcessPointerEvent(view, x, y, PointerEventType.Moved); - return; - } - double num = x - value.StartX; - double num2 = y - value.StartY; - if (Math.Sqrt(num * num + num2 * num2) >= 10.0) - { - ProcessPanGesture(view, num, num2, (GestureStatus)(value.IsPanning ? 1 : 0)); - value.IsPanning = true; - } - ProcessPointerEvent(view, x, y, PointerEventType.Moved); - } - - public static void ProcessPointerUp(View? view, double x, double y) - { - if (view == null) - { - return; - } - if (_gestureState.TryGetValue(view, out GestureTrackingState value)) - { - value.CurrentX = x; - value.CurrentY = y; - double num = x - value.StartX; - double num2 = y - value.StartY; - double num3 = Math.Sqrt(num * num + num2 * num2); - double totalMilliseconds = (DateTime.UtcNow - value.StartTime).TotalMilliseconds; - if (num3 >= 50.0 && totalMilliseconds <= 500.0) - { - SwipeDirection swipeDirection = DetermineSwipeDirection(num, num2); - if (swipeDirection != SwipeDirection.Right) - { - ProcessSwipeGesture(view, swipeDirection); - } - else if (Math.Abs(num) > Math.Abs(num2) * 0.5) - { - ProcessSwipeGesture(view, (!(num > 0.0)) ? SwipeDirection.Left : SwipeDirection.Right); - } - } - if (value.IsPanning) - { - ProcessPanGesture(view, num, num2, (GestureStatus)2); - } - else if (num3 < 15.0 && totalMilliseconds < 500.0) - { - Console.WriteLine($"[GestureManager] Detected tap on {((object)view).GetType().Name} (distance={num3:F1}, elapsed={totalMilliseconds:F0}ms)"); - ProcessTap(view, x, y); - } - _gestureState.Remove(view); - } - ProcessPointerEvent(view, x, y, PointerEventType.Released); - } - - public static void ProcessPointerEntered(View? view, double x, double y) - { - if (view != null) - { - ProcessPointerEvent(view, x, y, PointerEventType.Entered); - } - } - - public static void ProcessPointerExited(View? view, double x, double y) - { - if (view != null) - { - ProcessPointerEvent(view, x, y, PointerEventType.Exited); - } - } - - private static SwipeDirection DetermineSwipeDirection(double deltaX, double deltaY) - { - double num = Math.Abs(deltaX); - double num2 = Math.Abs(deltaY); - if (num > num2 * 0.5) - { - if (!(deltaX > 0.0)) - { - return SwipeDirection.Left; - } - return SwipeDirection.Right; - } - if (num2 > num * 0.5) - { - if (!(deltaY > 0.0)) - { - return SwipeDirection.Up; - } - return SwipeDirection.Down; - } - if (!(deltaX > 0.0)) - { - return SwipeDirection.Left; - } - return SwipeDirection.Right; - } - - private static void ProcessSwipeGesture(View view, SwipeDirection direction) - { - //IL_002a: Unknown result type (might be due to invalid IL or missing references) - IList gestureRecognizers = view.GestureRecognizers; - if (gestureRecognizers == null) - { - return; - } - foreach (IGestureRecognizer item in gestureRecognizers) - { - SwipeGestureRecognizer val = (SwipeGestureRecognizer)(object)((item is SwipeGestureRecognizer) ? item : null); - if (val == null || !((Enum)val.Direction).HasFlag((Enum)direction)) - { - continue; - } - Console.WriteLine($"[GestureManager] Swipe detected: {direction}"); - try - { - MethodInfo method = typeof(SwipeGestureRecognizer).GetMethod("SendSwiped", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); - if (method != null) - { - method.Invoke(val, new object[2] { view, direction }); - Console.WriteLine("[GestureManager] SendSwiped invoked successfully"); - } - } - catch (Exception ex) - { - Console.WriteLine("[GestureManager] SendSwiped failed: " + ex.Message); - } - ICommand command = val.Command; - if (command != null && command.CanExecute(val.CommandParameter)) - { - val.Command.Execute(val.CommandParameter); - } - } - } - - private static void ProcessPanGesture(View view, double totalX, double totalY, GestureStatus status) - { - //IL_0040: Unknown result type (might be due to invalid IL or missing references) - //IL_00c7: Unknown result type (might be due to invalid IL or missing references) - //IL_00cd: Expected I4, but got Unknown - IList gestureRecognizers = view.GestureRecognizers; - if (gestureRecognizers == null) - { - return; - } - foreach (IGestureRecognizer item in gestureRecognizers) - { - PanGestureRecognizer val = (PanGestureRecognizer)(object)((item is PanGestureRecognizer) ? item : null); - if (val == null) - { - continue; - } - Console.WriteLine($"[GestureManager] Pan gesture: status={status}, totalX={totalX:F1}, totalY={totalY:F1}"); - try - { - MethodInfo method = typeof(PanGestureRecognizer).GetMethod("SendPan", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); - if (method != null) - { - method.Invoke(val, new object[4] - { - view, - totalX, - totalY, - (int)status - }); - } - } - catch (Exception ex) - { - Console.WriteLine("[GestureManager] SendPan failed: " + ex.Message); - } - } - } - - private static void ProcessPointerEvent(View view, double x, double y, PointerEventType eventType) - { - IList gestureRecognizers = view.GestureRecognizers; - if (gestureRecognizers == null) - { - return; - } - foreach (IGestureRecognizer item in gestureRecognizers) - { - PointerGestureRecognizer val = (PointerGestureRecognizer)(object)((item is PointerGestureRecognizer) ? item : null); - if (val == null) - { - continue; - } - try - { - string text = eventType switch - { - PointerEventType.Entered => "SendPointerEntered", - PointerEventType.Exited => "SendPointerExited", - PointerEventType.Pressed => "SendPointerPressed", - PointerEventType.Moved => "SendPointerMoved", - PointerEventType.Released => "SendPointerReleased", - _ => null, - }; - if (text != null) - { - MethodInfo method = typeof(PointerGestureRecognizer).GetMethod(text, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); - if (method != null) - { - object obj = CreatePointerEventArgs(view, x, y); - method.Invoke(val, new object[2] { view, obj }); - } - } - } - catch (Exception ex) - { - Console.WriteLine("[GestureManager] Pointer event failed: " + ex.Message); - } - } - } - - private static object CreatePointerEventArgs(View view, double x, double y) - { - try - { - Type type = typeof(PointerGestureRecognizer).Assembly.GetType("Microsoft.Maui.Controls.PointerEventArgs"); - if (type != null) - { - ConstructorInfo constructorInfo = type.GetConstructors().FirstOrDefault(); - if (constructorInfo != null) - { - return constructorInfo.Invoke(new object[0]); - } - } - } - catch - { - } - return null; - } - - public static bool HasSwipeGestureRecognizer(View? view) - { - if (((view != null) ? view.GestureRecognizers : null) == null) - { - return false; - } - foreach (IGestureRecognizer gestureRecognizer in view.GestureRecognizers) - { - if (gestureRecognizer is SwipeGestureRecognizer) - { - return true; - } - } - return false; - } - - public static bool HasPanGestureRecognizer(View? view) - { - if (((view != null) ? view.GestureRecognizers : null) == null) - { - return false; - } - foreach (IGestureRecognizer gestureRecognizer in view.GestureRecognizers) - { - if (gestureRecognizer is PanGestureRecognizer) - { - return true; - } - } - return false; - } - - public static bool HasPointerGestureRecognizer(View? view) - { - if (((view != null) ? view.GestureRecognizers : null) == null) - { - return false; - } - foreach (IGestureRecognizer gestureRecognizer in view.GestureRecognizers) - { - if (gestureRecognizer is PointerGestureRecognizer) - { - return true; - } - } - return false; - } -} diff --git a/Handlers/GraphicsViewHandler.cs b/Handlers/GraphicsViewHandler.cs index 7f86ffd..40c832f 100644 --- a/Handlers/GraphicsViewHandler.cs +++ b/Handlers/GraphicsViewHandler.cs @@ -1,57 +1,62 @@ -using Microsoft.Maui.Graphics; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using Microsoft.Maui.Handlers; +using Microsoft.Maui.Graphics; +using SkiaSharp; namespace Microsoft.Maui.Platform.Linux.Handlers; -public class GraphicsViewHandler : ViewHandler +/// +/// Handler for GraphicsView on Linux using Skia rendering. +/// Maps IGraphicsView interface to SkiaGraphicsView platform view. +/// IGraphicsView has: Drawable, Invalidate() +/// +public partial class GraphicsViewHandler : ViewHandler { - public static IPropertyMapper Mapper = (IPropertyMapper)(object)new PropertyMapper((IPropertyMapper[])(object)new IPropertyMapper[1] { (IPropertyMapper)ViewHandler.ViewMapper }) - { - ["Drawable"] = MapDrawable, - ["Background"] = MapBackground - }; + public static IPropertyMapper Mapper = new PropertyMapper(ViewHandler.ViewMapper) + { + [nameof(IGraphicsView.Drawable)] = MapDrawable, + [nameof(IView.Background)] = MapBackground, + }; - public static CommandMapper CommandMapper = new CommandMapper((CommandMapper)(object)ViewHandler.ViewCommandMapper) { ["Invalidate"] = MapInvalidate }; + public static CommandMapper CommandMapper = new(ViewHandler.ViewCommandMapper) + { + [nameof(IGraphicsView.Invalidate)] = MapInvalidate, + }; - public GraphicsViewHandler() - : base((IPropertyMapper)(object)Mapper, (CommandMapper)(object)CommandMapper) - { - } + public GraphicsViewHandler() : base(Mapper, CommandMapper) + { + } - public GraphicsViewHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null) - : base((IPropertyMapper)(((object)mapper) ?? ((object)Mapper)), (CommandMapper)(((object)commandMapper) ?? ((object)CommandMapper))) - { - } + public GraphicsViewHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null) + : base(mapper ?? Mapper, commandMapper ?? CommandMapper) + { + } - protected override SkiaGraphicsView CreatePlatformView() - { - return new SkiaGraphicsView(); - } + protected override SkiaGraphicsView CreatePlatformView() + { + return new SkiaGraphicsView(); + } - public static void MapDrawable(GraphicsViewHandler handler, IGraphicsView graphicsView) - { - if (((ViewHandler)(object)handler).PlatformView != null) - { - ((ViewHandler)(object)handler).PlatformView.Drawable = graphicsView.Drawable; - } - } + public static void MapDrawable(GraphicsViewHandler handler, IGraphicsView graphicsView) + { + if (handler.PlatformView is null) return; + handler.PlatformView.Drawable = graphicsView.Drawable; + } - public static void MapBackground(GraphicsViewHandler handler, IGraphicsView graphicsView) - { - //IL_002c: Unknown result type (might be due to invalid IL or missing references) - if (((ViewHandler)(object)handler).PlatformView != null) - { - Paint background = ((IView)graphicsView).Background; - SolidPaint val = (SolidPaint)(object)((background is SolidPaint) ? background : null); - if (val != null && val.Color != null) - { - ((ViewHandler)(object)handler).PlatformView.BackgroundColor = val.Color.ToSKColor(); - } - } - } + public static void MapBackground(GraphicsViewHandler handler, IGraphicsView graphicsView) + { + if (handler.PlatformView is null) return; - public static void MapInvalidate(GraphicsViewHandler handler, IGraphicsView graphicsView, object? args) - { - ((ViewHandler)(object)handler).PlatformView?.Invalidate(); - } + if (graphicsView.Background is SolidPaint solidPaint && solidPaint.Color is not null) + { + handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor(); + } + } + + public static void MapInvalidate(GraphicsViewHandler handler, IGraphicsView graphicsView, object? args) + { + handler.PlatformView?.Invalidate(); + } } diff --git a/Handlers/GridHandler.cs b/Handlers/GridHandler.cs deleted file mode 100644 index 3b2183b..0000000 --- a/Handlers/GridHandler.cs +++ /dev/null @@ -1,170 +0,0 @@ -using System; -using System.Collections.Generic; -using Microsoft.Maui.Controls; -using Microsoft.Maui.Handlers; -using Microsoft.Maui.Platform.Linux.Hosting; -using SkiaSharp; - -namespace Microsoft.Maui.Platform.Linux.Handlers; - -public class GridHandler : LayoutHandler -{ - public new static IPropertyMapper Mapper = (IPropertyMapper)(object)new PropertyMapper((IPropertyMapper[])(object)new IPropertyMapper[1] { (IPropertyMapper)LayoutHandler.Mapper }) - { - ["RowSpacing"] = MapRowSpacing, - ["ColumnSpacing"] = MapColumnSpacing, - ["RowDefinitions"] = MapRowDefinitions, - ["ColumnDefinitions"] = MapColumnDefinitions - }; - - public GridHandler() - : base((IPropertyMapper?)(object)Mapper) - { - } - - protected override SkiaLayoutView CreatePlatformView() - { - return new SkiaGrid(); - } - - protected override void ConnectHandler(SkiaLayoutView platformView) - { - //IL_00d3: Unknown result type (might be due to invalid IL or missing references) - //IL_00d8: Unknown result type (might be due to invalid IL or missing references) - //IL_00fb: Unknown result type (might be due to invalid IL or missing references) - //IL_00bb: Unknown result type (might be due to invalid IL or missing references) - try - { - ILayout virtualView = ((ViewHandler)(object)this).VirtualView; - IGridLayout val = (IGridLayout)(object)((virtualView is IGridLayout) ? virtualView : null); - if (val == null || ((ElementHandler)this).MauiContext == null || !(platformView is SkiaGrid skiaGrid)) - { - return; - } - Console.WriteLine($"[GridHandler] ConnectHandler: {((ICollection)val).Count} children, {val.RowDefinitions.Count} rows, {val.ColumnDefinitions.Count} cols"); - ILayout virtualView2 = ((ViewHandler)(object)this).VirtualView; - VisualElement val2 = (VisualElement)(object)((virtualView2 is VisualElement) ? virtualView2 : null); - if (val2 != null && val2.BackgroundColor != null) - { - platformView.BackgroundColor = val2.BackgroundColor.ToSKColor(); - } - IPadding virtualView3 = (IPadding)(object)((ViewHandler)(object)this).VirtualView; - if (virtualView3 != null) - { - Thickness padding = virtualView3.Padding; - platformView.Padding = new SKRect((float)((Thickness)(ref padding)).Left, (float)((Thickness)(ref padding)).Top, (float)((Thickness)(ref padding)).Right, (float)((Thickness)(ref padding)).Bottom); - Console.WriteLine($"[GridHandler] Applied Padding: L={((Thickness)(ref padding)).Left}, T={((Thickness)(ref padding)).Top}, R={((Thickness)(ref padding)).Right}, B={((Thickness)(ref padding)).Bottom}"); - } - MapRowDefinitions(this, val); - MapColumnDefinitions(this, val); - for (int i = 0; i < ((ICollection)val).Count; i++) - { - IView val3 = ((IList)val)[i]; - if (val3 != null) - { - Console.WriteLine($"[GridHandler] Processing child {i}: {((object)val3).GetType().Name}"); - if (val3.Handler == null) - { - val3.Handler = val3.ToViewHandler(((ElementHandler)this).MauiContext); - } - int num = 0; - int num2 = 0; - int rowSpan = 1; - int columnSpan = 1; - View val4 = (View)(object)((val3 is View) ? val3 : null); - if (val4 != null) - { - num = Grid.GetRow((BindableObject)(object)val4); - num2 = Grid.GetColumn((BindableObject)(object)val4); - rowSpan = Grid.GetRowSpan((BindableObject)(object)val4); - columnSpan = Grid.GetColumnSpan((BindableObject)(object)val4); - } - Console.WriteLine($"[GridHandler] Child {i} at row={num}, col={num2}, handler={((object)val3.Handler)?.GetType().Name}"); - IViewHandler handler = val3.Handler; - if (((handler != null) ? ((IElementHandler)handler).PlatformView : null) is SkiaView child) - { - skiaGrid.AddChild(child, num, num2, rowSpan, columnSpan); - Console.WriteLine($"[GridHandler] Added child {i} to grid"); - } - } - } - Console.WriteLine("[GridHandler] ConnectHandler complete"); - } - catch (Exception ex) - { - Console.WriteLine("[GridHandler] EXCEPTION in ConnectHandler: " + ex.GetType().Name + ": " + ex.Message); - Console.WriteLine("[GridHandler] Stack trace: " + ex.StackTrace); - throw; - } - } - - public static void MapRowSpacing(GridHandler handler, IGridLayout layout) - { - if (((ViewHandler)(object)handler).PlatformView is SkiaGrid skiaGrid) - { - skiaGrid.RowSpacing = (float)layout.RowSpacing; - } - } - - public static void MapColumnSpacing(GridHandler handler, IGridLayout layout) - { - if (((ViewHandler)(object)handler).PlatformView is SkiaGrid skiaGrid) - { - skiaGrid.ColumnSpacing = (float)layout.ColumnSpacing; - } - } - - public static void MapRowDefinitions(GridHandler handler, IGridLayout layout) - { - //IL_002f: Unknown result type (might be due to invalid IL or missing references) - //IL_0034: Unknown result type (might be due to invalid IL or missing references) - if (!(((ViewHandler)(object)handler).PlatformView is SkiaGrid skiaGrid)) - { - return; - } - skiaGrid.RowDefinitions.Clear(); - foreach (IGridRowDefinition rowDefinition in layout.RowDefinitions) - { - GridLength height = rowDefinition.Height; - if (((GridLength)(ref height)).IsAbsolute) - { - skiaGrid.RowDefinitions.Add(new GridLength((float)((GridLength)(ref height)).Value)); - } - else if (((GridLength)(ref height)).IsAuto) - { - skiaGrid.RowDefinitions.Add(GridLength.Auto); - } - else - { - skiaGrid.RowDefinitions.Add(new GridLength((float)((GridLength)(ref height)).Value, GridUnitType.Star)); - } - } - } - - public static void MapColumnDefinitions(GridHandler handler, IGridLayout layout) - { - //IL_002f: Unknown result type (might be due to invalid IL or missing references) - //IL_0034: Unknown result type (might be due to invalid IL or missing references) - if (!(((ViewHandler)(object)handler).PlatformView is SkiaGrid skiaGrid)) - { - return; - } - skiaGrid.ColumnDefinitions.Clear(); - foreach (IGridColumnDefinition columnDefinition in layout.ColumnDefinitions) - { - GridLength width = columnDefinition.Width; - if (((GridLength)(ref width)).IsAbsolute) - { - skiaGrid.ColumnDefinitions.Add(new GridLength((float)((GridLength)(ref width)).Value)); - } - else if (((GridLength)(ref width)).IsAuto) - { - skiaGrid.ColumnDefinitions.Add(GridLength.Auto); - } - else - { - skiaGrid.ColumnDefinitions.Add(new GridLength((float)((GridLength)(ref width)).Value, GridUnitType.Star)); - } - } - } -} diff --git a/Handlers/GtkWebViewHandler.cs b/Handlers/GtkWebViewHandler.cs deleted file mode 100644 index 66290b7..0000000 --- a/Handlers/GtkWebViewHandler.cs +++ /dev/null @@ -1,240 +0,0 @@ -using System; -using Microsoft.Maui.Controls; -using Microsoft.Maui.Handlers; -using Microsoft.Maui.Platform.Linux.Native; -using Microsoft.Maui.Platform.Linux.Services; -using SkiaSharp; - -namespace Microsoft.Maui.Platform.Linux.Handlers; - -public class GtkWebViewHandler : ViewHandler -{ - private GtkWebViewPlatformView? _platformWebView; - - private bool _isRegisteredWithHost; - - private SKRect _lastBounds; - - public static IPropertyMapper Mapper = (IPropertyMapper)(object)new PropertyMapper((IPropertyMapper[])(object)new IPropertyMapper[1] { (IPropertyMapper)ViewHandler.ViewMapper }) { ["Source"] = MapSource }; - - public static CommandMapper CommandMapper = new CommandMapper((CommandMapper)(object)ViewHandler.ViewCommandMapper) - { - ["GoBack"] = MapGoBack, - ["GoForward"] = MapGoForward, - ["Reload"] = MapReload - }; - - public GtkWebViewHandler() - : base((IPropertyMapper)(object)Mapper, (CommandMapper)(object)CommandMapper) - { - } - - public GtkWebViewHandler(IPropertyMapper? mapper = null, CommandMapper? commandMapper = null) - : base((IPropertyMapper)(((object)mapper) ?? ((object)Mapper)), (CommandMapper)(((object)commandMapper) ?? ((object)CommandMapper))) - { - } - - protected override GtkWebViewProxy CreatePlatformView() - { - _platformWebView = new GtkWebViewPlatformView(); - return new GtkWebViewProxy(this, _platformWebView); - } - - protected override void ConnectHandler(GtkWebViewProxy platformView) - { - base.ConnectHandler(platformView); - if (_platformWebView != null) - { - _platformWebView.NavigationStarted += OnNavigationStarted; - _platformWebView.NavigationCompleted += OnNavigationCompleted; - } - Console.WriteLine("[GtkWebViewHandler] ConnectHandler - WebView ready"); - } - - protected override void DisconnectHandler(GtkWebViewProxy platformView) - { - if (_platformWebView != null) - { - _platformWebView.NavigationStarted -= OnNavigationStarted; - _platformWebView.NavigationCompleted -= OnNavigationCompleted; - UnregisterFromHost(); - _platformWebView.Dispose(); - _platformWebView = null; - } - base.DisconnectHandler(platformView); - } - - private void OnNavigationStarted(object? sender, string uri) - { - Console.WriteLine("[GtkWebViewHandler] Navigation started: " + uri); - try - { - GLibNative.IdleAdd(delegate - { - //IL_001c: Unknown result type (might be due to invalid IL or missing references) - //IL_0022: Expected O, but got Unknown - try - { - IWebView virtualView = base.VirtualView; - IWebViewController val = (IWebViewController)(object)((virtualView is IWebViewController) ? virtualView : null); - if (val != null) - { - WebNavigatingEventArgs e = new WebNavigatingEventArgs((WebNavigationEvent)3, (WebViewSource)null, uri); - val.SendNavigating(e); - Console.WriteLine("[GtkWebViewHandler] Sent Navigating event to VirtualView"); - } - } - catch (Exception ex2) - { - Console.WriteLine("[GtkWebViewHandler] Error in SendNavigating: " + ex2.Message); - } - return false; - }); - } - catch (Exception ex) - { - Console.WriteLine("[GtkWebViewHandler] Error dispatching navigation started: " + ex.Message); - } - } - - private void OnNavigationCompleted(object? sender, (string Url, bool Success) e) - { - Console.WriteLine($"[GtkWebViewHandler] Navigation completed: {e.Url} (Success: {e.Success})"); - try - { - GLibNative.IdleAdd(delegate - { - //IL_0028: Unknown result type (might be due to invalid IL or missing references) - //IL_0036: Unknown result type (might be due to invalid IL or missing references) - //IL_0037: Unknown result type (might be due to invalid IL or missing references) - //IL_003d: Expected O, but got Unknown - try - { - IWebView virtualView = base.VirtualView; - IWebViewController val = (IWebViewController)(object)((virtualView is IWebViewController) ? virtualView : null); - if (val != null) - { - WebNavigationResult val2 = (WebNavigationResult)(e.Success ? 1 : 4); - WebNavigatedEventArgs e2 = new WebNavigatedEventArgs((WebNavigationEvent)3, (WebViewSource)null, e.Url, val2); - val.SendNavigated(e2); - bool flag = _platformWebView?.CanGoBack() ?? false; - bool flag2 = _platformWebView?.CanGoForward() ?? false; - val.CanGoBack = flag; - val.CanGoForward = flag2; - Console.WriteLine($"[GtkWebViewHandler] Sent Navigated, CanGoBack={flag}, CanGoForward={flag2}"); - } - } - catch (Exception ex2) - { - Console.WriteLine("[GtkWebViewHandler] Error in SendNavigated: " + ex2.Message); - } - return false; - }); - } - catch (Exception ex) - { - Console.WriteLine("[GtkWebViewHandler] Error dispatching navigation completed: " + ex.Message); - } - } - - internal void RegisterWithHost(SKRect bounds) - { - //IL_0070: Unknown result type (might be due to invalid IL or missing references) - //IL_011c: Unknown result type (might be due to invalid IL or missing references) - //IL_011e: Unknown result type (might be due to invalid IL or missing references) - //IL_01b0: Unknown result type (might be due to invalid IL or missing references) - //IL_01b1: Unknown result type (might be due to invalid IL or missing references) - if (_platformWebView == null) - { - return; - } - GtkHostService instance = GtkHostService.Instance; - if (instance.HostWindow == null || instance.WebViewManager == null) - { - Console.WriteLine("[GtkWebViewHandler] Warning: GTK host not initialized, cannot register WebView"); - return; - } - int num = (int)((SKRect)(ref bounds)).Left; - int num2 = (int)((SKRect)(ref bounds)).Top; - int num3 = (int)((SKRect)(ref bounds)).Width; - int num4 = (int)((SKRect)(ref bounds)).Height; - if (num3 <= 0 || num4 <= 0) - { - Console.WriteLine($"[GtkWebViewHandler] Skipping invalid bounds: {bounds}"); - return; - } - if (!_isRegisteredWithHost) - { - instance.HostWindow.AddWebView(_platformWebView.Widget, num, num2, num3, num4); - _isRegisteredWithHost = true; - Console.WriteLine($"[GtkWebViewHandler] Registered WebView at ({num}, {num2}) size {num3}x{num4}"); - } - else if (bounds != _lastBounds) - { - instance.HostWindow.MoveResizeWebView(_platformWebView.Widget, num, num2, num3, num4); - Console.WriteLine($"[GtkWebViewHandler] Updated WebView to ({num}, {num2}) size {num3}x{num4}"); - } - _lastBounds = bounds; - } - - private void UnregisterFromHost() - { - if (_isRegisteredWithHost && _platformWebView != null) - { - GtkHostService instance = GtkHostService.Instance; - if (instance.HostWindow != null) - { - instance.HostWindow.RemoveWebView(_platformWebView.Widget); - Console.WriteLine("[GtkWebViewHandler] Unregistered WebView from host"); - } - _isRegisteredWithHost = false; - } - } - - public static void MapSource(GtkWebViewHandler handler, IWebView webView) - { - if (handler._platformWebView == null) - { - return; - } - IWebViewSource source = webView.Source; - Console.WriteLine("[GtkWebViewHandler] MapSource: " + (((object)source)?.GetType().Name ?? "null")); - UrlWebViewSource val = (UrlWebViewSource)(object)((source is UrlWebViewSource) ? source : null); - if (val != null) - { - string url = val.Url; - if (!string.IsNullOrEmpty(url)) - { - handler._platformWebView.Navigate(url); - } - return; - } - HtmlWebViewSource val2 = (HtmlWebViewSource)(object)((source is HtmlWebViewSource) ? source : null); - if (val2 != null) - { - string html = val2.Html; - if (!string.IsNullOrEmpty(html)) - { - handler._platformWebView.LoadHtml(html, val2.BaseUrl); - } - } - } - - public static void MapGoBack(GtkWebViewHandler handler, IWebView webView, object? args) - { - Console.WriteLine($"[GtkWebViewHandler] MapGoBack called, CanGoBack={handler._platformWebView?.CanGoBack()}"); - handler._platformWebView?.GoBack(); - } - - public static void MapGoForward(GtkWebViewHandler handler, IWebView webView, object? args) - { - Console.WriteLine($"[GtkWebViewHandler] MapGoForward called, CanGoForward={handler._platformWebView?.CanGoForward()}"); - handler._platformWebView?.GoForward(); - } - - public static void MapReload(GtkWebViewHandler handler, IWebView webView, object? args) - { - Console.WriteLine("[GtkWebViewHandler] MapReload called"); - handler._platformWebView?.Reload(); - } -} diff --git a/Handlers/GtkWebViewManager.cs b/Handlers/GtkWebViewManager.cs deleted file mode 100644 index 67ea8db..0000000 --- a/Handlers/GtkWebViewManager.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System.Collections.Generic; -using Microsoft.Maui.Platform.Linux.Window; - -namespace Microsoft.Maui.Platform.Linux.Handlers; - -public sealed class GtkWebViewManager -{ - private readonly GtkHostWindow _host; - - private readonly Dictionary _webViews = new Dictionary(); - - public GtkWebViewManager(GtkHostWindow host) - { - _host = host; - } - - public GtkWebViewPlatformView CreateWebView(object key, int x, int y, int width, int height) - { - GtkWebViewPlatformView gtkWebViewPlatformView = new GtkWebViewPlatformView(); - _webViews[key] = gtkWebViewPlatformView; - _host.AddWebView(gtkWebViewPlatformView.Widget, x, y, width, height); - return gtkWebViewPlatformView; - } - - public void UpdateLayout(object key, int x, int y, int width, int height) - { - if (_webViews.TryGetValue(key, out GtkWebViewPlatformView value)) - { - _host.MoveResizeWebView(value.Widget, x, y, width, height); - } - } - - public GtkWebViewPlatformView? GetWebView(object key) - { - if (!_webViews.TryGetValue(key, out GtkWebViewPlatformView value)) - { - return null; - } - return value; - } - - public void RemoveWebView(object key) - { - if (_webViews.TryGetValue(key, out GtkWebViewPlatformView value)) - { - _host.RemoveWebView(value.Widget); - value.Dispose(); - _webViews.Remove(key); - } - } - - public void Clear() - { - foreach (KeyValuePair webView in _webViews) - { - _host.RemoveWebView(webView.Value.Widget); - webView.Value.Dispose(); - } - _webViews.Clear(); - } -} diff --git a/Handlers/GtkWebViewPlatformView.cs b/Handlers/GtkWebViewPlatformView.cs deleted file mode 100644 index dc08fad..0000000 --- a/Handlers/GtkWebViewPlatformView.cs +++ /dev/null @@ -1,183 +0,0 @@ -using System; -using Microsoft.Maui.Platform.Linux.Native; - -namespace Microsoft.Maui.Platform.Linux.Handlers; - -public sealed class GtkWebViewPlatformView : IDisposable -{ - private IntPtr _widget; - - private bool _disposed; - - private string? _currentUri; - - private ulong _loadChangedSignalId; - - private WebKitNative.LoadChangedCallback? _loadChangedCallback; - - public IntPtr Widget => _widget; - - public string? CurrentUri => _currentUri; - - public event EventHandler? NavigationStarted; - - public event EventHandler<(string Url, bool Success)>? NavigationCompleted; - - public event EventHandler? TitleChanged; - - public GtkWebViewPlatformView() - { - if (!WebKitNative.Initialize()) - { - throw new InvalidOperationException("Failed to initialize WebKitGTK. Is libwebkit2gtk-4.x installed?"); - } - _widget = WebKitNative.WebViewNew(); - if (_widget == IntPtr.Zero) - { - throw new InvalidOperationException("Failed to create WebKitWebView widget"); - } - WebKitNative.ConfigureSettings(_widget); - _loadChangedCallback = OnLoadChanged; - _loadChangedSignalId = WebKitNative.ConnectLoadChanged(_widget, _loadChangedCallback); - Console.WriteLine("[GtkWebViewPlatformView] Created WebKitWebView widget"); - } - - private void OnLoadChanged(IntPtr webView, int loadEvent, IntPtr userData) - { - try - { - string text = WebKitNative.GetUri(webView) ?? _currentUri ?? ""; - switch ((WebKitNative.WebKitLoadEvent)loadEvent) - { - case WebKitNative.WebKitLoadEvent.Started: - Console.WriteLine("[GtkWebViewPlatformView] Load started: " + text); - this.NavigationStarted?.Invoke(this, text); - break; - case WebKitNative.WebKitLoadEvent.Finished: - _currentUri = text; - Console.WriteLine("[GtkWebViewPlatformView] Load finished: " + text); - this.NavigationCompleted?.Invoke(this, (text, true)); - break; - case WebKitNative.WebKitLoadEvent.Committed: - _currentUri = text; - Console.WriteLine("[GtkWebViewPlatformView] Load committed: " + text); - break; - case WebKitNative.WebKitLoadEvent.Redirected: - break; - } - } - catch (Exception ex) - { - Console.WriteLine("[GtkWebViewPlatformView] Error in OnLoadChanged: " + ex.Message); - Console.WriteLine("[GtkWebViewPlatformView] Stack trace: " + ex.StackTrace); - } - } - - public void Navigate(string uri) - { - if (_widget != IntPtr.Zero) - { - WebKitNative.LoadUri(_widget, uri); - Console.WriteLine("[GtkWebViewPlatformView] Navigate to: " + uri); - } - } - - public void LoadHtml(string html, string? baseUri = null) - { - if (_widget != IntPtr.Zero) - { - WebKitNative.LoadHtml(_widget, html, baseUri); - Console.WriteLine("[GtkWebViewPlatformView] Load HTML content"); - } - } - - public void GoBack() - { - if (_widget != IntPtr.Zero) - { - WebKitNative.GoBack(_widget); - } - } - - public void GoForward() - { - if (_widget != IntPtr.Zero) - { - WebKitNative.GoForward(_widget); - } - } - - public bool CanGoBack() - { - if (_widget == IntPtr.Zero) - { - return false; - } - return WebKitNative.CanGoBack(_widget); - } - - public bool CanGoForward() - { - if (_widget == IntPtr.Zero) - { - return false; - } - return WebKitNative.CanGoForward(_widget); - } - - public void Reload() - { - if (_widget != IntPtr.Zero) - { - WebKitNative.Reload(_widget); - } - } - - public void Stop() - { - if (_widget != IntPtr.Zero) - { - WebKitNative.StopLoading(_widget); - } - } - - public string? GetTitle() - { - if (_widget == IntPtr.Zero) - { - return null; - } - return WebKitNative.GetTitle(_widget); - } - - public string? GetUri() - { - if (_widget == IntPtr.Zero) - { - return null; - } - return WebKitNative.GetUri(_widget); - } - - public void SetJavascriptEnabled(bool enabled) - { - if (_widget != IntPtr.Zero) - { - WebKitNative.SetJavascriptEnabled(_widget, enabled); - } - } - - public void Dispose() - { - if (!_disposed) - { - _disposed = true; - if (_widget != IntPtr.Zero) - { - WebKitNative.DisconnectLoadChanged(_widget); - } - _widget = IntPtr.Zero; - _loadChangedCallback = null; - } - } -} diff --git a/Handlers/GtkWebViewProxy.cs b/Handlers/GtkWebViewProxy.cs deleted file mode 100644 index 97cc492..0000000 --- a/Handlers/GtkWebViewProxy.cs +++ /dev/null @@ -1,104 +0,0 @@ -using System; -using SkiaSharp; - -namespace Microsoft.Maui.Platform.Linux.Handlers; - -public class GtkWebViewProxy : SkiaView -{ - private readonly GtkWebViewHandler _handler; - - private readonly GtkWebViewPlatformView _platformView; - - public GtkWebViewPlatformView PlatformView => _platformView; - - public bool CanGoBack => _platformView.CanGoBack(); - - public bool CanGoForward => _platformView.CanGoForward(); - - public GtkWebViewProxy(GtkWebViewHandler handler, GtkWebViewPlatformView platformView) - { - _handler = handler; - _platformView = platformView; - } - - public override void Arrange(SKRect bounds) - { - //IL_0001: Unknown result type (might be due to invalid IL or missing references) - //IL_0008: Unknown result type (might be due to invalid IL or missing references) - //IL_0009: Unknown result type (might be due to invalid IL or missing references) - //IL_000e: Unknown result type (might be due to invalid IL or missing references) - //IL_0015: Unknown result type (might be due to invalid IL or missing references) - base.Arrange(bounds); - SKRect bounds2 = TransformToWindow(bounds); - _handler.RegisterWithHost(bounds2); - } - - private SKRect TransformToWindow(SKRect localBounds) - { - //IL_001b: Unknown result type (might be due to invalid IL or missing references) - //IL_0020: Unknown result type (might be due to invalid IL or missing references) - //IL_002c: Unknown result type (might be due to invalid IL or missing references) - //IL_0031: Unknown result type (might be due to invalid IL or missing references) - //IL_0059: Unknown result type (might be due to invalid IL or missing references) - float num = ((SKRect)(ref localBounds)).Left; - float num2 = ((SKRect)(ref localBounds)).Top; - for (SkiaView parent = base.Parent; parent != null; parent = parent.Parent) - { - float num3 = num; - SKRect bounds = parent.Bounds; - num = num3 + ((SKRect)(ref bounds)).Left; - float num4 = num2; - bounds = parent.Bounds; - num2 = num4 + ((SKRect)(ref bounds)).Top; - } - return new SKRect(num, num2, num + ((SKRect)(ref localBounds)).Width, num2 + ((SKRect)(ref localBounds)).Height); - } - - public override void Draw(SKCanvas canvas) - { - //IL_0000: Unknown result type (might be due to invalid IL or missing references) - //IL_0005: Unknown result type (might be due to invalid IL or missing references) - //IL_000a: Unknown result type (might be due to invalid IL or missing references) - //IL_0014: Unknown result type (might be due to invalid IL or missing references) - //IL_001c: Expected O, but got Unknown - //IL_001e: Unknown result type (might be due to invalid IL or missing references) - SKPaint val = new SKPaint - { - Color = new SKColor((byte)0, (byte)0, (byte)0, (byte)0), - Style = (SKPaintStyle)0 - }; - try - { - canvas.DrawRect(base.Bounds, val); - } - finally - { - ((IDisposable)val)?.Dispose(); - } - } - - public void Navigate(string url) - { - _platformView.Navigate(url); - } - - public void LoadHtml(string html, string? baseUrl = null) - { - _platformView.LoadHtml(html, baseUrl); - } - - public void GoBack() - { - _platformView.GoBack(); - } - - public void GoForward() - { - _platformView.GoForward(); - } - - public void Reload() - { - _platformView.Reload(); - } -} diff --git a/Handlers/ImageButtonHandler.cs b/Handlers/ImageButtonHandler.cs index c6099f0..cf7bdf1 100644 --- a/Handlers/ImageButtonHandler.cs +++ b/Handlers/ImageButtonHandler.cs @@ -1,275 +1,233 @@ -using System; -using System.IO; -using System.Threading; -using Microsoft.Maui.Controls; -using Microsoft.Maui.Graphics; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using Microsoft.Maui.Handlers; +using Microsoft.Maui.Graphics; +using SkiaSharp; namespace Microsoft.Maui.Platform.Linux.Handlers; -public class ImageButtonHandler : ViewHandler +/// +/// Handler for ImageButton on Linux using Skia rendering. +/// Maps IImageButton interface to SkiaImageButton platform view. +/// IImageButton extends: IImage, IView, IButtonStroke, IPadding +/// +public partial class ImageButtonHandler : ViewHandler { - internal class ImageSourceServiceResultManager - { - private readonly ImageButtonHandler _handler; + public static IPropertyMapper Mapper = new PropertyMapper(ViewHandler.ViewMapper) + { + [nameof(IImage.Aspect)] = MapAspect, + [nameof(IImage.IsOpaque)] = MapIsOpaque, + [nameof(IImageSourcePart.Source)] = MapSource, + [nameof(IButtonStroke.StrokeColor)] = MapStrokeColor, + [nameof(IButtonStroke.StrokeThickness)] = MapStrokeThickness, + [nameof(IButtonStroke.CornerRadius)] = MapCornerRadius, + [nameof(IPadding.Padding)] = MapPadding, + [nameof(IView.Background)] = MapBackground, + }; - private CancellationTokenSource? _cts; + public static CommandMapper CommandMapper = new(ViewHandler.ViewCommandMapper) + { + }; - public ImageSourceServiceResultManager(ImageButtonHandler handler) - { - _handler = handler; - } + public ImageButtonHandler() : base(Mapper, CommandMapper) + { + } - public async void UpdateImageSourceAsync() - { - _cts?.Cancel(); - _cts = new CancellationTokenSource(); - CancellationToken token = _cts.Token; - try - { - IImageButton virtualView = ((ViewHandler)(object)_handler).VirtualView; - IImageSource val = ((virtualView != null) ? ((IImageSourcePart)virtualView).Source : null); - if (val == null) - { - ((ViewHandler)(object)_handler).PlatformView?.LoadFromData(Array.Empty()); - return; - } - IImageSourcePart virtualView2 = (IImageSourcePart)(object)((ViewHandler)(object)_handler).VirtualView; - if (virtualView2 != null) - { - virtualView2.UpdateIsLoading(true); - } - IFileImageSource val2 = (IFileImageSource)(object)((val is IFileImageSource) ? val : null); - if (val2 != null) - { - string file = val2.File; - if (!string.IsNullOrEmpty(file)) - { - await ((ViewHandler)(object)_handler).PlatformView.LoadFromFileAsync(file); - } - return; - } - IUriImageSource val3 = (IUriImageSource)(object)((val is IUriImageSource) ? val : null); - if (val3 != null) - { - Uri uri = val3.Uri; - if (uri != null) - { - await ((ViewHandler)(object)_handler).PlatformView.LoadFromUriAsync(uri); - } - return; - } - IStreamImageSource val4 = (IStreamImageSource)(object)((val is IStreamImageSource) ? val : null); - if (val4 != null) - { - Stream stream = await val4.GetStreamAsync(token); - if (stream != null) - { - await ((ViewHandler)(object)_handler).PlatformView.LoadFromStreamAsync(stream); - } - } - } - catch (OperationCanceledException) - { - } - catch (Exception) - { - IImageSourcePart virtualView3 = (IImageSourcePart)(object)((ViewHandler)(object)_handler).VirtualView; - if (virtualView3 != null) - { - virtualView3.UpdateIsLoading(false); - } - } - } - } + public ImageButtonHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null) + : base(mapper ?? Mapper, commandMapper ?? CommandMapper) + { + } - public static IPropertyMapper Mapper = (IPropertyMapper)(object)new PropertyMapper((IPropertyMapper[])(object)new IPropertyMapper[1] { (IPropertyMapper)ViewHandler.ViewMapper }) - { - ["Aspect"] = MapAspect, - ["IsOpaque"] = MapIsOpaque, - ["Source"] = MapSource, - ["StrokeColor"] = MapStrokeColor, - ["StrokeThickness"] = MapStrokeThickness, - ["CornerRadius"] = MapCornerRadius, - ["Padding"] = MapPadding, - ["Background"] = MapBackground, - ["BackgroundColor"] = MapBackgroundColor - }; + protected override SkiaImageButton CreatePlatformView() + { + return new SkiaImageButton(); + } - public static CommandMapper CommandMapper = new CommandMapper((CommandMapper)(object)ViewHandler.ViewCommandMapper); + protected override void ConnectHandler(SkiaImageButton platformView) + { + base.ConnectHandler(platformView); + platformView.Clicked += OnClicked; + platformView.Pressed += OnPressed; + platformView.Released += OnReleased; + platformView.ImageLoaded += OnImageLoaded; + platformView.ImageLoadingError += OnImageLoadingError; + } - private ImageSourceServiceResultManager _sourceLoader; + protected override void DisconnectHandler(SkiaImageButton platformView) + { + platformView.Clicked -= OnClicked; + platformView.Pressed -= OnPressed; + platformView.Released -= OnReleased; + platformView.ImageLoaded -= OnImageLoaded; + platformView.ImageLoadingError -= OnImageLoadingError; + base.DisconnectHandler(platformView); + } - private ImageSourceServiceResultManager SourceLoader => _sourceLoader ?? (_sourceLoader = new ImageSourceServiceResultManager(this)); + private void OnClicked(object? sender, EventArgs e) + { + VirtualView?.Clicked(); + } - public ImageButtonHandler() - : base((IPropertyMapper)(object)Mapper, (CommandMapper)(object)CommandMapper) - { - } + private void OnPressed(object? sender, EventArgs e) + { + VirtualView?.Pressed(); + } - public ImageButtonHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null) - : base((IPropertyMapper)(((object)mapper) ?? ((object)Mapper)), (CommandMapper)(((object)commandMapper) ?? ((object)CommandMapper))) - { - } + private void OnReleased(object? sender, EventArgs e) + { + VirtualView?.Released(); + } - protected override SkiaImageButton CreatePlatformView() - { - return new SkiaImageButton(); - } + private void OnImageLoaded(object? sender, EventArgs e) + { + if (VirtualView is IImageSourcePart imageSourcePart) + { + imageSourcePart.UpdateIsLoading(false); + } + } - protected override void ConnectHandler(SkiaImageButton platformView) - { - base.ConnectHandler(platformView); - platformView.Clicked += OnClicked; - platformView.Pressed += OnPressed; - platformView.Released += OnReleased; - platformView.ImageLoaded += OnImageLoaded; - platformView.ImageLoadingError += OnImageLoadingError; - } + private void OnImageLoadingError(object? sender, ImageLoadingErrorEventArgs e) + { + if (VirtualView is IImageSourcePart imageSourcePart) + { + imageSourcePart.UpdateIsLoading(false); + } + } - protected override void DisconnectHandler(SkiaImageButton platformView) - { - platformView.Clicked -= OnClicked; - platformView.Pressed -= OnPressed; - platformView.Released -= OnReleased; - platformView.ImageLoaded -= OnImageLoaded; - platformView.ImageLoadingError -= OnImageLoadingError; - base.DisconnectHandler(platformView); - } + public static void MapAspect(ImageButtonHandler handler, IImageButton imageButton) + { + if (handler.PlatformView is null) return; + handler.PlatformView.Aspect = imageButton.Aspect; + } - private void OnClicked(object? sender, EventArgs e) - { - IImageButton virtualView = base.VirtualView; - if (virtualView != null) - { - ((IButton)virtualView).Clicked(); - } - } + public static void MapIsOpaque(ImageButtonHandler handler, IImageButton imageButton) + { + if (handler.PlatformView is null) return; + handler.PlatformView.IsOpaque = imageButton.IsOpaque; + } - private void OnPressed(object? sender, EventArgs e) - { - IImageButton virtualView = base.VirtualView; - if (virtualView != null) - { - ((IButton)virtualView).Pressed(); - } - } + public static void MapSource(ImageButtonHandler handler, IImageButton imageButton) + { + if (handler.PlatformView is null) return; + handler.SourceLoader.UpdateImageSourceAsync(); + } - private void OnReleased(object? sender, EventArgs e) - { - IImageButton virtualView = base.VirtualView; - if (virtualView != null) - { - ((IButton)virtualView).Released(); - } - } + public static void MapStrokeColor(ImageButtonHandler handler, IImageButton imageButton) + { + if (handler.PlatformView is null) return; - private void OnImageLoaded(object? sender, EventArgs e) - { - IImageSourcePart virtualView = (IImageSourcePart)(object)base.VirtualView; - if (virtualView != null) - { - virtualView.UpdateIsLoading(false); - } - } + if (imageButton.StrokeColor is not null) + handler.PlatformView.StrokeColor = imageButton.StrokeColor.ToSKColor(); + } - private void OnImageLoadingError(object? sender, ImageLoadingErrorEventArgs e) - { - IImageSourcePart virtualView = (IImageSourcePart)(object)base.VirtualView; - if (virtualView != null) - { - virtualView.UpdateIsLoading(false); - } - } + public static void MapStrokeThickness(ImageButtonHandler handler, IImageButton imageButton) + { + if (handler.PlatformView is null) return; + handler.PlatformView.StrokeThickness = (float)imageButton.StrokeThickness; + } - public static void MapAspect(ImageButtonHandler handler, IImageButton imageButton) - { - //IL_0010: Unknown result type (might be due to invalid IL or missing references) - if (((ViewHandler)(object)handler).PlatformView != null) - { - ((ViewHandler)(object)handler).PlatformView.Aspect = ((IImage)imageButton).Aspect; - } - } + public static void MapCornerRadius(ImageButtonHandler handler, IImageButton imageButton) + { + if (handler.PlatformView is null) return; + handler.PlatformView.CornerRadius = imageButton.CornerRadius; + } - public static void MapIsOpaque(ImageButtonHandler handler, IImageButton imageButton) - { - if (((ViewHandler)(object)handler).PlatformView != null) - { - ((ViewHandler)(object)handler).PlatformView.IsOpaque = ((IImage)imageButton).IsOpaque; - } - } + public static void MapPadding(ImageButtonHandler handler, IImageButton imageButton) + { + if (handler.PlatformView is null) return; - public static void MapSource(ImageButtonHandler handler, IImageButton imageButton) - { - if (((ViewHandler)(object)handler).PlatformView != null) - { - handler.SourceLoader.UpdateImageSourceAsync(); - } - } + var padding = imageButton.Padding; + handler.PlatformView.PaddingLeft = (float)padding.Left; + handler.PlatformView.PaddingTop = (float)padding.Top; + handler.PlatformView.PaddingRight = (float)padding.Right; + handler.PlatformView.PaddingBottom = (float)padding.Bottom; + } - public static void MapStrokeColor(ImageButtonHandler handler, IImageButton imageButton) - { - //IL_001d: Unknown result type (might be due to invalid IL or missing references) - if (((ViewHandler)(object)handler).PlatformView != null && ((IButtonStroke)imageButton).StrokeColor != null) - { - ((ViewHandler)(object)handler).PlatformView.StrokeColor = ((IButtonStroke)imageButton).StrokeColor.ToSKColor(); - } - } + public static void MapBackground(ImageButtonHandler handler, IImageButton imageButton) + { + if (handler.PlatformView is null) return; - public static void MapStrokeThickness(ImageButtonHandler handler, IImageButton imageButton) - { - if (((ViewHandler)(object)handler).PlatformView != null) - { - ((ViewHandler)(object)handler).PlatformView.StrokeThickness = (float)((IButtonStroke)imageButton).StrokeThickness; - } - } + if (imageButton.Background is SolidPaint solidPaint && solidPaint.Color is not null) + { + handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor(); + } + } - public static void MapCornerRadius(ImageButtonHandler handler, IImageButton imageButton) - { - if (((ViewHandler)(object)handler).PlatformView != null) - { - ((ViewHandler)(object)handler).PlatformView.CornerRadius = ((IButtonStroke)imageButton).CornerRadius; - } - } + // Image source loading helper + private ImageSourceServiceResultManager _sourceLoader = null!; - public static void MapPadding(ImageButtonHandler handler, IImageButton imageButton) - { - //IL_000a: Unknown result type (might be due to invalid IL or missing references) - //IL_000f: Unknown result type (might be due to invalid IL or missing references) - if (((ViewHandler)(object)handler).PlatformView != null) - { - Thickness padding = ((IPadding)imageButton).Padding; - ((ViewHandler)(object)handler).PlatformView.PaddingLeft = (float)((Thickness)(ref padding)).Left; - ((ViewHandler)(object)handler).PlatformView.PaddingTop = (float)((Thickness)(ref padding)).Top; - ((ViewHandler)(object)handler).PlatformView.PaddingRight = (float)((Thickness)(ref padding)).Right; - ((ViewHandler)(object)handler).PlatformView.PaddingBottom = (float)((Thickness)(ref padding)).Bottom; - } - } + private ImageSourceServiceResultManager SourceLoader => + _sourceLoader ??= new ImageSourceServiceResultManager(this); - public static void MapBackground(ImageButtonHandler handler, IImageButton imageButton) - { - //IL_002c: Unknown result type (might be due to invalid IL or missing references) - if (((ViewHandler)(object)handler).PlatformView != null) - { - Paint background = ((IView)imageButton).Background; - SolidPaint val = (SolidPaint)(object)((background is SolidPaint) ? background : null); - if (val != null && val.Color != null) - { - ((ViewHandler)(object)handler).PlatformView.BackgroundColor = val.Color.ToSKColor(); - } - } - } + internal class ImageSourceServiceResultManager + { + private readonly ImageButtonHandler _handler; + private CancellationTokenSource? _cts; - public static void MapBackgroundColor(ImageButtonHandler handler, IImageButton imageButton) - { - //IL_0027: Unknown result type (might be due to invalid IL or missing references) - if (((ViewHandler)(object)handler).PlatformView != null) - { - ImageButton val = (ImageButton)(object)((imageButton is ImageButton) ? imageButton : null); - if (val != null && ((VisualElement)val).BackgroundColor != null) - { - ((ViewHandler)(object)handler).PlatformView.BackgroundColor = ((VisualElement)val).BackgroundColor.ToSKColor(); - } - } - } + public ImageSourceServiceResultManager(ImageButtonHandler handler) + { + _handler = handler; + } + + public async void UpdateImageSourceAsync() + { + _cts?.Cancel(); + _cts = new CancellationTokenSource(); + var token = _cts.Token; + + try + { + var source = _handler.VirtualView?.Source; + if (source == null) + { + _handler.PlatformView?.LoadFromData(Array.Empty()); + return; + } + + if (_handler.VirtualView is IImageSourcePart imageSourcePart) + { + imageSourcePart.UpdateIsLoading(true); + } + + // Handle different image source types + if (source is IFileImageSource fileSource) + { + var file = fileSource.File; + if (!string.IsNullOrEmpty(file)) + { + await _handler.PlatformView!.LoadFromFileAsync(file); + } + } + else if (source is IUriImageSource uriSource) + { + var uri = uriSource.Uri; + if (uri != null) + { + await _handler.PlatformView!.LoadFromUriAsync(uri); + } + } + else if (source is IStreamImageSource streamSource) + { + var stream = await streamSource.GetStreamAsync(token); + if (stream != null) + { + await _handler.PlatformView!.LoadFromStreamAsync(stream); + } + } + } + catch (OperationCanceledException) + { + // Loading was cancelled + } + catch (Exception) + { + // Handle error + if (_handler.VirtualView is IImageSourcePart imageSourcePart) + { + imageSourcePart.UpdateIsLoading(false); + } + } + } + } } diff --git a/Handlers/ImageHandler.cs b/Handlers/ImageHandler.cs index 9c7f2d0..ce6e3e8 100644 --- a/Handlers/ImageHandler.cs +++ b/Handlers/ImageHandler.cs @@ -1,342 +1,180 @@ -using System; -using System.IO; -using System.Threading; -using Microsoft.Maui.Controls; -using Microsoft.Maui.Graphics; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using Microsoft.Maui.Handlers; +using Microsoft.Maui.Graphics; using SkiaSharp; namespace Microsoft.Maui.Platform.Linux.Handlers; -public class ImageHandler : ViewHandler +/// +/// Handler for Image on Linux using Skia rendering. +/// Maps IImage interface to SkiaImage platform view. +/// IImage has: Aspect, IsOpaque (inherits from IImageSourcePart) +/// +public partial class ImageHandler : ViewHandler { - internal class ImageSourceServiceResultManager - { - private readonly ImageHandler _handler; + public static IPropertyMapper Mapper = new PropertyMapper(ViewHandler.ViewMapper) + { + [nameof(IImage.Aspect)] = MapAspect, + [nameof(IImage.IsOpaque)] = MapIsOpaque, + [nameof(IImageSourcePart.Source)] = MapSource, + [nameof(IView.Background)] = MapBackground, + }; - private CancellationTokenSource? _cts; + public static CommandMapper CommandMapper = new(ViewHandler.ViewCommandMapper) + { + }; - public ImageSourceServiceResultManager(ImageHandler handler) - { - _handler = handler; - } + public ImageHandler() : base(Mapper, CommandMapper) + { + } - public async void UpdateImageSourceAsync() - { - _cts?.Cancel(); - _cts = new CancellationTokenSource(); - CancellationToken token = _cts.Token; - try - { - IImage virtualView = ((ViewHandler)(object)_handler).VirtualView; - IImageSource val = ((virtualView != null) ? ((IImageSourcePart)virtualView).Source : null); - if (val == null) - { - ((ViewHandler)(object)_handler).PlatformView?.LoadFromData(Array.Empty()); - return; - } - IImageSourcePart virtualView2 = (IImageSourcePart)(object)((ViewHandler)(object)_handler).VirtualView; - if (virtualView2 != null) - { - virtualView2.UpdateIsLoading(true); - } - IFileImageSource val2 = (IFileImageSource)(object)((val is IFileImageSource) ? val : null); - if (val2 != null) - { - string file = val2.File; - if (!string.IsNullOrEmpty(file)) - { - await ((ViewHandler)(object)_handler).PlatformView.LoadFromFileAsync(file); - } - return; - } - IUriImageSource val3 = (IUriImageSource)(object)((val is IUriImageSource) ? val : null); - if (val3 != null) - { - Uri uri = val3.Uri; - if (uri != null) - { - await ((ViewHandler)(object)_handler).PlatformView.LoadFromUriAsync(uri); - } - return; - } - IStreamImageSource val4 = (IStreamImageSource)(object)((val is IStreamImageSource) ? val : null); - if (val4 != null) - { - Stream stream = await val4.GetStreamAsync(token); - if (stream != null) - { - await ((ViewHandler)(object)_handler).PlatformView.LoadFromStreamAsync(stream); - } - return; - } - FontImageSource val5 = (FontImageSource)(object)((val is FontImageSource) ? val : null); - if (val5 != null) - { - SKBitmap val6 = RenderFontImageSource(val5, ((ViewHandler)(object)_handler).PlatformView.WidthRequest, ((ViewHandler)(object)_handler).PlatformView.HeightRequest); - if (val6 != null) - { - ((ViewHandler)(object)_handler).PlatformView.LoadFromBitmap(val6); - } - } - } - catch (OperationCanceledException) - { - } - catch (Exception) - { - IImageSourcePart virtualView3 = (IImageSourcePart)(object)((ViewHandler)(object)_handler).VirtualView; - if (virtualView3 != null) - { - virtualView3.UpdateIsLoading(false); - } - } - } + public ImageHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null) + : base(mapper ?? Mapper, commandMapper ?? CommandMapper) + { + } - private static SKBitmap? RenderFontImageSource(FontImageSource fontSource, double requestedWidth, double requestedHeight) - { - //IL_0062: Unknown result type (might be due to invalid IL or missing references) - //IL_005b: Unknown result type (might be due to invalid IL or missing references) - //IL_0067: Unknown result type (might be due to invalid IL or missing references) - //IL_006b: Unknown result type (might be due to invalid IL or missing references) - //IL_0071: Expected O, but got Unknown - //IL_0072: Unknown result type (might be due to invalid IL or missing references) - //IL_0079: Expected O, but got Unknown - //IL_007b: Unknown result type (might be due to invalid IL or missing references) - //IL_016a: Unknown result type (might be due to invalid IL or missing references) - //IL_0171: Expected O, but got Unknown - //IL_0173: Unknown result type (might be due to invalid IL or missing references) - //IL_0178: Unknown result type (might be due to invalid IL or missing references) - //IL_0179: Unknown result type (might be due to invalid IL or missing references) - //IL_017f: Unknown result type (might be due to invalid IL or missing references) - //IL_0186: Unknown result type (might be due to invalid IL or missing references) - //IL_018f: Expected O, but got Unknown - //IL_0191: Unknown result type (might be due to invalid IL or missing references) - string glyph = fontSource.Glyph; - if (string.IsNullOrEmpty(glyph)) - { - return null; - } - int val = (int)Math.Max((requestedWidth > 0.0) ? requestedWidth : 24.0, (requestedHeight > 0.0) ? requestedHeight : 24.0); - val = Math.Max(val, 16); - SKColor color = fontSource.Color?.ToSKColor() ?? SKColors.Black; - SKBitmap val2 = new SKBitmap(val, val, false); - SKCanvas val3 = new SKCanvas(val2); - try - { - val3.Clear(SKColors.Transparent); - SKTypeface val4 = null; - if (!string.IsNullOrEmpty(fontSource.FontFamily)) - { - string[] array = new string[4] - { - "/usr/share/fonts/truetype/" + fontSource.FontFamily + ".ttf", - "/usr/share/fonts/opentype/" + fontSource.FontFamily + ".otf", - "/usr/local/share/fonts/" + fontSource.FontFamily + ".ttf", - Path.Combine(AppContext.BaseDirectory, fontSource.FontFamily + ".ttf") - }; - foreach (string text in array) - { - if (File.Exists(text)) - { - val4 = SKTypeface.FromFile(text, 0); - if (val4 != null) - { - break; - } - } - } - if (val4 == null) - { - val4 = SKTypeface.FromFamilyName(fontSource.FontFamily); - } - } - if (val4 == null) - { - val4 = SKTypeface.Default; - } - float num = (float)val * 0.8f; - SKFont val5 = new SKFont(val4, num, 1f, 0f); - try - { - SKPaint val6 = new SKPaint(val5) - { - Color = color, - IsAntialias = true, - TextAlign = (SKTextAlign)1 - }; - try - { - SKRect val7 = default(SKRect); - val6.MeasureText(glyph, ref val7); - float num2 = (float)val / 2f; - float num3 = ((float)val - ((SKRect)(ref val7)).Top - ((SKRect)(ref val7)).Bottom) / 2f; - val3.DrawText(glyph, num2, num3, val6); - return val2; - } - finally - { - ((IDisposable)val6)?.Dispose(); - } - } - finally - { - ((IDisposable)val5)?.Dispose(); - } - } - finally - { - ((IDisposable)val3)?.Dispose(); - } - } - } + protected override SkiaImage CreatePlatformView() + { + return new SkiaImage(); + } - public static IPropertyMapper Mapper = (IPropertyMapper)(object)new PropertyMapper((IPropertyMapper[])(object)new IPropertyMapper[1] { (IPropertyMapper)ViewHandler.ViewMapper }) - { - ["Aspect"] = MapAspect, - ["IsOpaque"] = MapIsOpaque, - ["Source"] = MapSource, - ["Background"] = MapBackground, - ["Width"] = MapWidth, - ["Height"] = MapHeight - }; + protected override void ConnectHandler(SkiaImage platformView) + { + base.ConnectHandler(platformView); + platformView.ImageLoaded += OnImageLoaded; + platformView.ImageLoadingError += OnImageLoadingError; + } - public static CommandMapper CommandMapper = new CommandMapper((CommandMapper)(object)ViewHandler.ViewCommandMapper); + protected override void DisconnectHandler(SkiaImage platformView) + { + platformView.ImageLoaded -= OnImageLoaded; + platformView.ImageLoadingError -= OnImageLoadingError; + base.DisconnectHandler(platformView); + } - private ImageSourceServiceResultManager _sourceLoader; + private void OnImageLoaded(object? sender, EventArgs e) + { + // Notify that the image has been loaded + if (VirtualView is IImageSourcePart imageSourcePart) + { + imageSourcePart.UpdateIsLoading(false); + } + } - private ImageSourceServiceResultManager SourceLoader => _sourceLoader ?? (_sourceLoader = new ImageSourceServiceResultManager(this)); + private void OnImageLoadingError(object? sender, ImageLoadingErrorEventArgs e) + { + // Handle loading error + if (VirtualView is IImageSourcePart imageSourcePart) + { + imageSourcePart.UpdateIsLoading(false); + } + } - public ImageHandler() - : base((IPropertyMapper)(object)Mapper, (CommandMapper)(object)CommandMapper) - { - } + public static void MapAspect(ImageHandler handler, IImage image) + { + if (handler.PlatformView is null) return; + handler.PlatformView.Aspect = image.Aspect; + } - public ImageHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null) - : base((IPropertyMapper)(((object)mapper) ?? ((object)Mapper)), (CommandMapper)(((object)commandMapper) ?? ((object)CommandMapper))) - { - } + public static void MapIsOpaque(ImageHandler handler, IImage image) + { + if (handler.PlatformView is null) return; + handler.PlatformView.IsOpaque = image.IsOpaque; + } - protected override SkiaImage CreatePlatformView() - { - return new SkiaImage(); - } + public static void MapSource(ImageHandler handler, IImage image) + { + if (handler.PlatformView is null) return; - protected override void ConnectHandler(SkiaImage platformView) - { - base.ConnectHandler(platformView); - platformView.ImageLoaded += OnImageLoaded; - platformView.ImageLoadingError += OnImageLoadingError; - } + handler.SourceLoader.UpdateImageSourceAsync(); + } - protected override void DisconnectHandler(SkiaImage platformView) - { - platformView.ImageLoaded -= OnImageLoaded; - platformView.ImageLoadingError -= OnImageLoadingError; - base.DisconnectHandler(platformView); - } + public static void MapBackground(ImageHandler handler, IImage image) + { + if (handler.PlatformView is null) return; - private void OnImageLoaded(object? sender, EventArgs e) - { - IImageSourcePart virtualView = (IImageSourcePart)(object)base.VirtualView; - if (virtualView != null) - { - virtualView.UpdateIsLoading(false); - } - } + if (image.Background is SolidPaint solidPaint && solidPaint.Color is not null) + { + handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor(); + } + } - private void OnImageLoadingError(object? sender, ImageLoadingErrorEventArgs e) - { - IImageSourcePart virtualView = (IImageSourcePart)(object)base.VirtualView; - if (virtualView != null) - { - virtualView.UpdateIsLoading(false); - } - } + // Image source loading helper + private ImageSourceServiceResultManager _sourceLoader = null!; - public static void MapAspect(ImageHandler handler, IImage image) - { - //IL_0010: Unknown result type (might be due to invalid IL or missing references) - if (((ViewHandler)(object)handler).PlatformView != null) - { - ((ViewHandler)(object)handler).PlatformView.Aspect = image.Aspect; - } - } + private ImageSourceServiceResultManager SourceLoader => + _sourceLoader ??= new ImageSourceServiceResultManager(this); - public static void MapIsOpaque(ImageHandler handler, IImage image) - { - if (((ViewHandler)(object)handler).PlatformView != null) - { - ((ViewHandler)(object)handler).PlatformView.IsOpaque = image.IsOpaque; - } - } + internal class ImageSourceServiceResultManager + { + private readonly ImageHandler _handler; + private CancellationTokenSource? _cts; - public static void MapSource(ImageHandler handler, IImage image) - { - if (((ViewHandler)(object)handler).PlatformView == null) - { - return; - } - Image val = (Image)(object)((image is Image) ? image : null); - if (val != null) - { - if (((VisualElement)val).WidthRequest > 0.0) - { - ((ViewHandler)(object)handler).PlatformView.WidthRequest = ((VisualElement)val).WidthRequest; - } - if (((VisualElement)val).HeightRequest > 0.0) - { - ((ViewHandler)(object)handler).PlatformView.HeightRequest = ((VisualElement)val).HeightRequest; - } - } - handler.SourceLoader.UpdateImageSourceAsync(); - } + public ImageSourceServiceResultManager(ImageHandler handler) + { + _handler = handler; + } - public static void MapBackground(ImageHandler handler, IImage image) - { - //IL_002c: Unknown result type (might be due to invalid IL or missing references) - if (((ViewHandler)(object)handler).PlatformView != null) - { - Paint background = ((IView)image).Background; - SolidPaint val = (SolidPaint)(object)((background is SolidPaint) ? background : null); - if (val != null && val.Color != null) - { - ((ViewHandler)(object)handler).PlatformView.BackgroundColor = val.Color.ToSKColor(); - } - } - } + public async void UpdateImageSourceAsync() + { + _cts?.Cancel(); + _cts = new CancellationTokenSource(); + var token = _cts.Token; - public static void MapWidth(ImageHandler handler, IImage image) - { - if (((ViewHandler)(object)handler).PlatformView != null) - { - Image val = (Image)(object)((image is Image) ? image : null); - if (val != null && ((VisualElement)val).WidthRequest > 0.0) - { - ((ViewHandler)(object)handler).PlatformView.WidthRequest = ((VisualElement)val).WidthRequest; - Console.WriteLine($"[ImageHandler] MapWidth: {((VisualElement)val).WidthRequest}"); - } - else if (((IView)image).Width > 0.0) - { - ((ViewHandler)(object)handler).PlatformView.WidthRequest = ((IView)image).Width; - } - } - } + try + { + var source = _handler.VirtualView?.Source; + if (source == null) + { + _handler.PlatformView?.LoadFromData(Array.Empty()); + return; + } - public static void MapHeight(ImageHandler handler, IImage image) - { - if (((ViewHandler)(object)handler).PlatformView != null) - { - Image val = (Image)(object)((image is Image) ? image : null); - if (val != null && ((VisualElement)val).HeightRequest > 0.0) - { - ((ViewHandler)(object)handler).PlatformView.HeightRequest = ((VisualElement)val).HeightRequest; - Console.WriteLine($"[ImageHandler] MapHeight: {((VisualElement)val).HeightRequest}"); - } - else if (((IView)image).Height > 0.0) - { - ((ViewHandler)(object)handler).PlatformView.HeightRequest = ((IView)image).Height; - } - } - } + if (_handler.VirtualView is IImageSourcePart imageSourcePart) + { + imageSourcePart.UpdateIsLoading(true); + } + + // Handle different image source types + if (source is IFileImageSource fileSource) + { + var file = fileSource.File; + if (!string.IsNullOrEmpty(file)) + { + await _handler.PlatformView!.LoadFromFileAsync(file); + } + } + else if (source is IUriImageSource uriSource) + { + var uri = uriSource.Uri; + if (uri != null) + { + await _handler.PlatformView!.LoadFromUriAsync(uri); + } + } + else if (source is IStreamImageSource streamSource) + { + var stream = await streamSource.GetStreamAsync(token); + if (stream != null) + { + await _handler.PlatformView!.LoadFromStreamAsync(stream); + } + } + } + catch (OperationCanceledException) + { + // Loading was cancelled + } + catch (Exception) + { + // Handle error + if (_handler.VirtualView is IImageSourcePart imageSourcePart) + { + imageSourcePart.UpdateIsLoading(false); + } + } + } + } } diff --git a/Handlers/ItemsViewHandler.cs b/Handlers/ItemsViewHandler.cs index aa673a9..0fb2e65 100644 --- a/Handlers/ItemsViewHandler.cs +++ b/Handlers/ItemsViewHandler.cs @@ -1,175 +1,164 @@ -using Microsoft.Maui.Controls; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using Microsoft.Maui.Handlers; +using Microsoft.Maui.Graphics; +using Microsoft.Maui.Controls; +using Microsoft.Maui.Platform; using SkiaSharp; namespace Microsoft.Maui.Platform.Linux.Handlers; -public class ItemsViewHandler : ViewHandler where TItemsView : ItemsView +/// +/// Base handler for ItemsView on Linux using Skia rendering. +/// Maps ItemsView to SkiaItemsView platform view. +/// +public partial class ItemsViewHandler : ViewHandler + where TItemsView : ItemsView { - public static IPropertyMapper> ItemsViewMapper = (IPropertyMapper>)(object)new PropertyMapper>((IPropertyMapper[])(object)new IPropertyMapper[1] { (IPropertyMapper)ViewHandler.ViewMapper }) - { - ["ItemsSource"] = MapItemsSource, - ["ItemTemplate"] = MapItemTemplate, - ["EmptyView"] = MapEmptyView, - ["EmptyViewTemplate"] = MapEmptyViewTemplate, - ["HorizontalScrollBarVisibility"] = MapHorizontalScrollBarVisibility, - ["VerticalScrollBarVisibility"] = MapVerticalScrollBarVisibility, - ["Background"] = MapBackground - }; + public static IPropertyMapper> ItemsViewMapper = + new PropertyMapper>(ViewHandler.ViewMapper) + { + [nameof(ItemsView.ItemsSource)] = MapItemsSource, + [nameof(ItemsView.ItemTemplate)] = MapItemTemplate, + [nameof(ItemsView.EmptyView)] = MapEmptyView, + [nameof(ItemsView.EmptyViewTemplate)] = MapEmptyViewTemplate, + [nameof(ItemsView.HorizontalScrollBarVisibility)] = MapHorizontalScrollBarVisibility, + [nameof(ItemsView.VerticalScrollBarVisibility)] = MapVerticalScrollBarVisibility, + [nameof(IView.Background)] = MapBackground, + }; - public static CommandMapper> ItemsViewCommandMapper = new CommandMapper>((CommandMapper)(object)ViewHandler.ViewCommandMapper) { ["ScrollTo"] = MapScrollTo }; + public static CommandMapper> ItemsViewCommandMapper = + new(ViewHandler.ViewCommandMapper) + { + ["ScrollTo"] = MapScrollTo, + }; - public ItemsViewHandler() - : base((IPropertyMapper)(object)ItemsViewMapper, (CommandMapper)(object)ItemsViewCommandMapper) - { - } + public ItemsViewHandler() : base(ItemsViewMapper, ItemsViewCommandMapper) + { + } - public ItemsViewHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null) - : base((IPropertyMapper)(((object)mapper) ?? ((object)ItemsViewMapper)), (CommandMapper)(((object)commandMapper) ?? ((object)ItemsViewCommandMapper))) - { - } + public ItemsViewHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null) + : base(mapper ?? ItemsViewMapper, commandMapper ?? ItemsViewCommandMapper) + { + } - protected override SkiaItemsView CreatePlatformView() - { - return new SkiaItemsView(); - } + protected override SkiaItemsView CreatePlatformView() + { + return new SkiaItemsView(); + } - protected override void ConnectHandler(SkiaItemsView platformView) - { - base.ConnectHandler(platformView); - platformView.Scrolled += OnScrolled; - platformView.ItemTapped += OnItemTapped; - platformView.ItemRenderer = RenderItem; - } + protected override void ConnectHandler(SkiaItemsView platformView) + { + base.ConnectHandler(platformView); + platformView.Scrolled += OnScrolled; + platformView.ItemTapped += OnItemTapped; - protected override void DisconnectHandler(SkiaItemsView platformView) - { - platformView.Scrolled -= OnScrolled; - platformView.ItemTapped -= OnItemTapped; - platformView.ItemRenderer = null; - base.DisconnectHandler(platformView); - } + // Set up item renderer + platformView.ItemRenderer = RenderItem; + } - private void OnScrolled(object? sender, ItemsScrolledEventArgs e) - { - //IL_0010: Unknown result type (might be due to invalid IL or missing references) - //IL_0015: Unknown result type (might be due to invalid IL or missing references) - //IL_0022: Unknown result type (might be due to invalid IL or missing references) - //IL_0031: Unknown result type (might be due to invalid IL or missing references) - //IL_0040: Unknown result type (might be due to invalid IL or missing references) - //IL_0054: Expected O, but got Unknown - object obj = base.VirtualView; - if (obj != null) - { - ((ItemsView)obj).SendScrolled(new ItemsViewScrolledEventArgs - { - VerticalOffset = e.ScrollOffset, - VerticalDelta = 0.0, - HorizontalOffset = 0.0, - HorizontalDelta = 0.0 - }); - } - } + protected override void DisconnectHandler(SkiaItemsView platformView) + { + platformView.Scrolled -= OnScrolled; + platformView.ItemTapped -= OnItemTapped; + platformView.ItemRenderer = null; + base.DisconnectHandler(platformView); + } - private void OnItemTapped(object? sender, ItemsViewItemTappedEventArgs e) - { - } + private void OnScrolled(object? sender, ItemsScrolledEventArgs e) + { + // Fire scrolled event on virtual view + VirtualView?.SendScrolled(new ItemsViewScrolledEventArgs + { + VerticalOffset = e.ScrollOffset, + VerticalDelta = 0, + HorizontalOffset = 0, + HorizontalDelta = 0 + }); + } - protected virtual bool RenderItem(object item, int index, SKRect bounds, SKCanvas canvas, SKPaint paint) - { - object obj = base.VirtualView; - if (obj != null) - { - _ = ((ItemsView)obj).ItemTemplate; - } - else - _ = null; - return false; - } + private void OnItemTapped(object? sender, ItemsViewItemTappedEventArgs e) + { + // Item tap handling - can be extended for selection + } - public static void MapItemsSource(ItemsViewHandler handler, TItemsView itemsView) - { - if (((ViewHandler)(object)handler).PlatformView != null) - { - ((ViewHandler)(object)handler).PlatformView.ItemsSource = ((ItemsView)itemsView).ItemsSource; - } - } + protected virtual bool RenderItem(object item, int index, SKRect bounds, SKCanvas canvas, SKPaint paint) + { + // Check if we have an ItemTemplate + var template = VirtualView?.ItemTemplate; + if (template == null) + return false; // Use default rendering - public static void MapItemTemplate(ItemsViewHandler handler, TItemsView itemsView) - { - ((ViewHandler)(object)handler).PlatformView?.Invalidate(); - } + // For now, render based on item ToString + // Full DataTemplate support would require creating actual views + return false; + } - public static void MapEmptyView(ItemsViewHandler handler, TItemsView itemsView) - { - if (((ViewHandler)(object)handler).PlatformView != null) - { - ((ViewHandler)(object)handler).PlatformView.EmptyView = ((ItemsView)itemsView).EmptyView; - if (((ItemsView)itemsView).EmptyView is string emptyViewText) - { - ((ViewHandler)(object)handler).PlatformView.EmptyViewText = emptyViewText; - } - } - } + public static void MapItemsSource(ItemsViewHandler handler, TItemsView itemsView) + { + if (handler.PlatformView is null) return; + handler.PlatformView.ItemsSource = itemsView.ItemsSource; + } - public static void MapEmptyViewTemplate(ItemsViewHandler handler, TItemsView itemsView) - { - ((ViewHandler)(object)handler).PlatformView?.Invalidate(); - } + public static void MapItemTemplate(ItemsViewHandler handler, TItemsView itemsView) + { + // ItemTemplate affects how items are rendered + // The renderer will check this when drawing items + handler.PlatformView?.Invalidate(); + } - public static void MapHorizontalScrollBarVisibility(ItemsViewHandler handler, TItemsView itemsView) - { - //IL_0015: Unknown result type (might be due to invalid IL or missing references) - //IL_001f: Expected I4, but got Unknown - if (((ViewHandler)(object)handler).PlatformView != null) - { - ((ViewHandler)(object)handler).PlatformView.HorizontalScrollBarVisibility = (ScrollBarVisibility)((ItemsView)itemsView).HorizontalScrollBarVisibility; - } - } + public static void MapEmptyView(ItemsViewHandler handler, TItemsView itemsView) + { + if (handler.PlatformView is null) return; - public static void MapVerticalScrollBarVisibility(ItemsViewHandler handler, TItemsView itemsView) - { - //IL_0015: Unknown result type (might be due to invalid IL or missing references) - //IL_001f: Expected I4, but got Unknown - if (((ViewHandler)(object)handler).PlatformView != null) - { - ((ViewHandler)(object)handler).PlatformView.VerticalScrollBarVisibility = (ScrollBarVisibility)((ItemsView)itemsView).VerticalScrollBarVisibility; - } - } + handler.PlatformView.EmptyView = itemsView.EmptyView; + if (itemsView.EmptyView is string text) + { + handler.PlatformView.EmptyViewText = text; + } + } - public static void MapBackground(ItemsViewHandler handler, TItemsView itemsView) - { - //IL_0029: Unknown result type (might be due to invalid IL or missing references) - if (((ViewHandler)(object)handler).PlatformView != null) - { - Brush background = ((VisualElement)(object)itemsView).Background; - SolidColorBrush val = (SolidColorBrush)(object)((background is SolidColorBrush) ? background : null); - if (val != null) - { - ((ViewHandler)(object)handler).PlatformView.BackgroundColor = val.Color.ToSKColor(); - } - } - } + public static void MapEmptyViewTemplate(ItemsViewHandler handler, TItemsView itemsView) + { + // EmptyViewTemplate would be used to render custom empty view + handler.PlatformView?.Invalidate(); + } - public static void MapScrollTo(ItemsViewHandler handler, TItemsView itemsView, object? args) - { - //IL_0014: Unknown result type (might be due to invalid IL or missing references) - //IL_001a: Invalid comparison between Unknown and I4 - if (((ViewHandler)(object)handler).PlatformView == null) - { - return; - } - ScrollToRequestEventArgs e = (ScrollToRequestEventArgs)((args is ScrollToRequestEventArgs) ? args : null); - if (e != null) - { - if ((int)e.Mode == 1) - { - ((ViewHandler)(object)handler).PlatformView.ScrollToIndex(e.Index, e.IsAnimated); - } - else if (e.Item != null) - { - ((ViewHandler)(object)handler).PlatformView.ScrollToItem(e.Item, e.IsAnimated); - } - } - } + public static void MapHorizontalScrollBarVisibility(ItemsViewHandler handler, TItemsView itemsView) + { + if (handler.PlatformView is null) return; + handler.PlatformView.HorizontalScrollBarVisibility = (ScrollBarVisibility)itemsView.HorizontalScrollBarVisibility; + } + + public static void MapVerticalScrollBarVisibility(ItemsViewHandler handler, TItemsView itemsView) + { + if (handler.PlatformView is null) return; + handler.PlatformView.VerticalScrollBarVisibility = (ScrollBarVisibility)itemsView.VerticalScrollBarVisibility; + } + + public static void MapBackground(ItemsViewHandler handler, TItemsView itemsView) + { + if (handler.PlatformView is null) return; + + if (itemsView.Background is SolidColorBrush solidBrush) + { + handler.PlatformView.BackgroundColor = solidBrush.Color.ToSKColor(); + } + } + + public static void MapScrollTo(ItemsViewHandler handler, TItemsView itemsView, object? args) + { + if (handler.PlatformView is null || args is not ScrollToRequestEventArgs scrollArgs) + return; + + if (scrollArgs.Mode == ScrollToMode.Position) + { + handler.PlatformView.ScrollToIndex(scrollArgs.Index, scrollArgs.IsAnimated); + } + else if (scrollArgs.Item != null) + { + handler.PlatformView.ScrollToItem(scrollArgs.Item, scrollArgs.IsAnimated); + } + } } diff --git a/Handlers/LabelHandler.cs b/Handlers/LabelHandler.cs index 6363193..a94953f 100644 --- a/Handlers/LabelHandler.cs +++ b/Handlers/LabelHandler.cs @@ -1,388 +1,208 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.Maui.Controls; -using Microsoft.Maui.Graphics; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using Microsoft.Maui.Handlers; -using Microsoft.Maui.Platform.Linux.Window; -using Microsoft.Maui.Primitives; +using Microsoft.Maui.Graphics; using SkiaSharp; namespace Microsoft.Maui.Platform.Linux.Handlers; -public class LabelHandler : ViewHandler +/// +/// Handler for Label on Linux using Skia rendering. +/// Maps ILabel interface to SkiaLabel platform view. +/// +public partial class LabelHandler : ViewHandler { - public static IPropertyMapper Mapper = (IPropertyMapper)(object)new PropertyMapper((IPropertyMapper[])(object)new IPropertyMapper[1] { (IPropertyMapper)ViewHandler.ViewMapper }) - { - ["Text"] = MapText, - ["TextColor"] = MapTextColor, - ["Font"] = MapFont, - ["CharacterSpacing"] = MapCharacterSpacing, - ["HorizontalTextAlignment"] = MapHorizontalTextAlignment, - ["VerticalTextAlignment"] = MapVerticalTextAlignment, - ["TextDecorations"] = MapTextDecorations, - ["LineHeight"] = MapLineHeight, - ["LineBreakMode"] = MapLineBreakMode, - ["MaxLines"] = MapMaxLines, - ["Padding"] = MapPadding, - ["Background"] = MapBackground, - ["VerticalLayoutAlignment"] = MapVerticalLayoutAlignment, - ["HorizontalLayoutAlignment"] = MapHorizontalLayoutAlignment, - ["FormattedText"] = MapFormattedText - }; + public static IPropertyMapper Mapper = new PropertyMapper(ViewHandler.ViewMapper) + { + [nameof(IText.Text)] = MapText, + [nameof(ITextStyle.TextColor)] = MapTextColor, + [nameof(ITextStyle.Font)] = MapFont, + [nameof(ITextStyle.CharacterSpacing)] = MapCharacterSpacing, + [nameof(ITextAlignment.HorizontalTextAlignment)] = MapHorizontalTextAlignment, + [nameof(ITextAlignment.VerticalTextAlignment)] = MapVerticalTextAlignment, + [nameof(ILabel.TextDecorations)] = MapTextDecorations, + [nameof(ILabel.LineHeight)] = MapLineHeight, + ["LineBreakMode"] = MapLineBreakMode, + ["MaxLines"] = MapMaxLines, + [nameof(IPadding.Padding)] = MapPadding, + [nameof(IView.Background)] = MapBackground, + [nameof(IView.VerticalLayoutAlignment)] = MapVerticalLayoutAlignment, + [nameof(IView.HorizontalLayoutAlignment)] = MapHorizontalLayoutAlignment, + }; - public static CommandMapper CommandMapper = new CommandMapper((CommandMapper)(object)ViewHandler.ViewCommandMapper); + public static CommandMapper CommandMapper = new(ViewHandler.ViewCommandMapper) + { + }; - public LabelHandler() - : base((IPropertyMapper)(object)Mapper, (CommandMapper)(object)CommandMapper) - { - } + public LabelHandler() : base(Mapper, CommandMapper) + { + } - public LabelHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null) - : base((IPropertyMapper)(((object)mapper) ?? ((object)Mapper)), (CommandMapper)(((object)commandMapper) ?? ((object)CommandMapper))) - { - } + public LabelHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null) + : base(mapper ?? Mapper, commandMapper ?? CommandMapper) + { + } - protected override SkiaLabel CreatePlatformView() - { - return new SkiaLabel(); - } + protected override SkiaLabel CreatePlatformView() + { + return new SkiaLabel(); + } - protected override void ConnectHandler(SkiaLabel platformView) - { - base.ConnectHandler(platformView); - ILabel virtualView = base.VirtualView; - View val = (View)(object)((virtualView is View) ? virtualView : null); - if (val != null) - { - platformView.MauiView = val; - if (val.GestureRecognizers.OfType().Any()) - { - platformView.CursorType = CursorType.Hand; - } - } - platformView.Tapped += OnPlatformViewTapped; - } + public static void MapText(LabelHandler handler, ILabel label) + { + if (handler.PlatformView is null) return; + handler.PlatformView.Text = label.Text ?? string.Empty; + } - protected override void DisconnectHandler(SkiaLabel platformView) - { - platformView.Tapped -= OnPlatformViewTapped; - platformView.MauiView = null; - base.DisconnectHandler(platformView); - } + public static void MapTextColor(LabelHandler handler, ILabel label) + { + if (handler.PlatformView is null) return; - private void OnPlatformViewTapped(object? sender, EventArgs e) - { - ILabel virtualView = base.VirtualView; - View val = (View)(object)((virtualView is View) ? virtualView : null); - if (val != null) - { - GestureManager.ProcessTap(val, 0.0, 0.0); - } - } + if (label.TextColor is not null) + handler.PlatformView.TextColor = label.TextColor.ToSKColor(); + } - public static void MapText(LabelHandler handler, ILabel label) - { - if (((ViewHandler)(object)handler).PlatformView != null) - { - ((ViewHandler)(object)handler).PlatformView.Text = ((IText)label).Text ?? string.Empty; - } - } + public static void MapFont(LabelHandler handler, ILabel label) + { + if (handler.PlatformView is null) return; - public static void MapTextColor(LabelHandler handler, ILabel label) - { - //IL_001d: Unknown result type (might be due to invalid IL or missing references) - if (((ViewHandler)(object)handler).PlatformView != null && ((ITextStyle)label).TextColor != null) - { - ((ViewHandler)(object)handler).PlatformView.TextColor = ((ITextStyle)label).TextColor.ToSKColor(); - } - } + var font = label.Font; + if (font.Size > 0) + handler.PlatformView.FontSize = (float)font.Size; - public static void MapFont(LabelHandler handler, ILabel label) - { - //IL_000a: Unknown result type (might be due to invalid IL or missing references) - //IL_000f: Unknown result type (might be due to invalid IL or missing references) - //IL_005d: Unknown result type (might be due to invalid IL or missing references) - //IL_0067: Invalid comparison between Unknown and I4 - //IL_0079: Unknown result type (might be due to invalid IL or missing references) - //IL_007f: Invalid comparison between Unknown and I4 - //IL_0083: Unknown result type (might be due to invalid IL or missing references) - //IL_0089: Invalid comparison between Unknown and I4 - if (((ViewHandler)(object)handler).PlatformView != null) - { - Font font = ((ITextStyle)label).Font; - if (((Font)(ref font)).Size > 0.0) - { - ((ViewHandler)(object)handler).PlatformView.FontSize = (float)((Font)(ref font)).Size; - } - if (!string.IsNullOrEmpty(((Font)(ref font)).Family)) - { - ((ViewHandler)(object)handler).PlatformView.FontFamily = ((Font)(ref font)).Family; - } - ((ViewHandler)(object)handler).PlatformView.IsBold = (int)((Font)(ref font)).Weight >= 700; - ((ViewHandler)(object)handler).PlatformView.IsItalic = (int)((Font)(ref font)).Slant == 1 || (int)((Font)(ref font)).Slant == 2; - } - } + if (!string.IsNullOrEmpty(font.Family)) + handler.PlatformView.FontFamily = font.Family; - public static void MapCharacterSpacing(LabelHandler handler, ILabel label) - { - if (((ViewHandler)(object)handler).PlatformView != null) - { - ((ViewHandler)(object)handler).PlatformView.CharacterSpacing = (float)((ITextStyle)label).CharacterSpacing; - } - } + handler.PlatformView.IsBold = font.Weight >= FontWeight.Bold; + handler.PlatformView.IsItalic = font.Slant == FontSlant.Italic || font.Slant == FontSlant.Oblique; + } - public static void MapHorizontalTextAlignment(LabelHandler handler, ILabel label) - { - //IL_0011: Unknown result type (might be due to invalid IL or missing references) - //IL_0016: Unknown result type (might be due to invalid IL or missing references) - //IL_0017: Unknown result type (might be due to invalid IL or missing references) - //IL_0029: Expected I4, but got Unknown - if (((ViewHandler)(object)handler).PlatformView != null) - { - SkiaLabel platformView = ((ViewHandler)(object)handler).PlatformView; - TextAlignment horizontalTextAlignment = ((ITextAlignment)label).HorizontalTextAlignment; - platformView.HorizontalTextAlignment = (int)horizontalTextAlignment switch - { - 0 => TextAlignment.Start, - 1 => TextAlignment.Center, - 2 => TextAlignment.End, - _ => TextAlignment.Start, - }; - } - } + public static void MapCharacterSpacing(LabelHandler handler, ILabel label) + { + if (handler.PlatformView is null) return; + handler.PlatformView.CharacterSpacing = (float)label.CharacterSpacing; + } - public static void MapVerticalTextAlignment(LabelHandler handler, ILabel label) - { - //IL_0011: Unknown result type (might be due to invalid IL or missing references) - //IL_0016: Unknown result type (might be due to invalid IL or missing references) - //IL_0017: Unknown result type (might be due to invalid IL or missing references) - //IL_0029: Expected I4, but got Unknown - if (((ViewHandler)(object)handler).PlatformView != null) - { - SkiaLabel platformView = ((ViewHandler)(object)handler).PlatformView; - TextAlignment verticalTextAlignment = ((ITextAlignment)label).VerticalTextAlignment; - platformView.VerticalTextAlignment = (int)verticalTextAlignment switch - { - 0 => TextAlignment.Start, - 1 => TextAlignment.Center, - 2 => TextAlignment.End, - _ => TextAlignment.Center, - }; - } - } + public static void MapHorizontalTextAlignment(LabelHandler handler, ILabel label) + { + if (handler.PlatformView is null) return; - public static void MapTextDecorations(LabelHandler handler, ILabel label) - { - //IL_0010: Unknown result type (might be due to invalid IL or missing references) - //IL_0016: Unknown result type (might be due to invalid IL or missing references) - //IL_0018: Invalid comparison between Unknown and I4 - //IL_0026: Unknown result type (might be due to invalid IL or missing references) - //IL_002c: Unknown result type (might be due to invalid IL or missing references) - //IL_002e: Invalid comparison between Unknown and I4 - if (((ViewHandler)(object)handler).PlatformView != null) - { - ((ViewHandler)(object)handler).PlatformView.IsUnderline = (label.TextDecorations & 1) > 0; - ((ViewHandler)(object)handler).PlatformView.IsStrikethrough = (label.TextDecorations & 2) > 0; - } - } + // Map MAUI TextAlignment to our internal TextAlignment + handler.PlatformView.HorizontalTextAlignment = label.HorizontalTextAlignment switch + { + Microsoft.Maui.TextAlignment.Start => Platform.TextAlignment.Start, + Microsoft.Maui.TextAlignment.Center => Platform.TextAlignment.Center, + Microsoft.Maui.TextAlignment.End => Platform.TextAlignment.End, + _ => Platform.TextAlignment.Start + }; + } - public static void MapLineHeight(LabelHandler handler, ILabel label) - { - if (((ViewHandler)(object)handler).PlatformView != null) - { - ((ViewHandler)(object)handler).PlatformView.LineHeight = (float)label.LineHeight; - } - } + public static void MapVerticalTextAlignment(LabelHandler handler, ILabel label) + { + if (handler.PlatformView is null) return; - public static void MapLineBreakMode(LabelHandler handler, ILabel label) - { - //IL_001b: Unknown result type (might be due to invalid IL or missing references) - //IL_0020: Unknown result type (might be due to invalid IL or missing references) - //IL_0021: Unknown result type (might be due to invalid IL or missing references) - //IL_003f: Expected I4, but got Unknown - if (((ViewHandler)(object)handler).PlatformView != null) - { - Label val = (Label)(object)((label is Label) ? label : null); - if (val != null) - { - SkiaLabel platformView = ((ViewHandler)(object)handler).PlatformView; - LineBreakMode lineBreakMode = val.LineBreakMode; - platformView.LineBreakMode = (int)lineBreakMode switch - { - 0 => LineBreakMode.NoWrap, - 1 => LineBreakMode.WordWrap, - 2 => LineBreakMode.CharacterWrap, - 3 => LineBreakMode.HeadTruncation, - 4 => LineBreakMode.TailTruncation, - 5 => LineBreakMode.MiddleTruncation, - _ => LineBreakMode.TailTruncation, - }; - } - } - } + handler.PlatformView.VerticalTextAlignment = label.VerticalTextAlignment switch + { + Microsoft.Maui.TextAlignment.Start => Platform.TextAlignment.Start, + Microsoft.Maui.TextAlignment.Center => Platform.TextAlignment.Center, + Microsoft.Maui.TextAlignment.End => Platform.TextAlignment.End, + _ => Platform.TextAlignment.Center + }; + } - public static void MapMaxLines(LabelHandler handler, ILabel label) - { - if (((ViewHandler)(object)handler).PlatformView != null) - { - Label val = (Label)(object)((label is Label) ? label : null); - if (val != null) - { - ((ViewHandler)(object)handler).PlatformView.MaxLines = val.MaxLines; - } - } - } + public static void MapTextDecorations(LabelHandler handler, ILabel label) + { + if (handler.PlatformView is null) return; - public static void MapPadding(LabelHandler handler, ILabel label) - { - //IL_000a: Unknown result type (might be due to invalid IL or missing references) - //IL_000f: Unknown result type (might be due to invalid IL or missing references) - //IL_0036: Unknown result type (might be due to invalid IL or missing references) - if (((ViewHandler)(object)handler).PlatformView != null) - { - Thickness padding = ((IPadding)label).Padding; - ((ViewHandler)(object)handler).PlatformView.Padding = new SKRect((float)((Thickness)(ref padding)).Left, (float)((Thickness)(ref padding)).Top, (float)((Thickness)(ref padding)).Right, (float)((Thickness)(ref padding)).Bottom); - } - } + handler.PlatformView.IsUnderline = (label.TextDecorations & TextDecorations.Underline) != 0; + handler.PlatformView.IsStrikethrough = (label.TextDecorations & TextDecorations.Strikethrough) != 0; + } - public static void MapBackground(LabelHandler handler, ILabel label) - { - //IL_002c: Unknown result type (might be due to invalid IL or missing references) - if (((ViewHandler)(object)handler).PlatformView != null) - { - Paint background = ((IView)label).Background; - SolidPaint val = (SolidPaint)(object)((background is SolidPaint) ? background : null); - if (val != null && val.Color != null) - { - ((ViewHandler)(object)handler).PlatformView.BackgroundColor = val.Color.ToSKColor(); - } - } - } + public static void MapLineHeight(LabelHandler handler, ILabel label) + { + if (handler.PlatformView is null) return; + handler.PlatformView.LineHeight = (float)label.LineHeight; + } - public static void MapVerticalLayoutAlignment(LabelHandler handler, ILabel label) - { - //IL_0011: Unknown result type (might be due to invalid IL or missing references) - //IL_0016: Unknown result type (might be due to invalid IL or missing references) - //IL_0017: Unknown result type (might be due to invalid IL or missing references) - //IL_002d: Expected I4, but got Unknown - //IL_0047: Unknown result type (might be due to invalid IL or missing references) - //IL_004c: Unknown result type (might be due to invalid IL or missing references) - //IL_002f: Unknown result type (might be due to invalid IL or missing references) - //IL_0034: Unknown result type (might be due to invalid IL or missing references) - //IL_0037: Unknown result type (might be due to invalid IL or missing references) - //IL_003c: Unknown result type (might be due to invalid IL or missing references) - //IL_003f: Unknown result type (might be due to invalid IL or missing references) - //IL_0044: Unknown result type (might be due to invalid IL or missing references) - //IL_0056: Unknown result type (might be due to invalid IL or missing references) - //IL_004f: Unknown result type (might be due to invalid IL or missing references) - //IL_0054: Unknown result type (might be due to invalid IL or missing references) - if (((ViewHandler)(object)handler).PlatformView != null) - { - SkiaLabel platformView = ((ViewHandler)(object)handler).PlatformView; - LayoutAlignment verticalLayoutAlignment = ((IView)label).VerticalLayoutAlignment; - platformView.VerticalOptions = (LayoutOptions)((int)verticalLayoutAlignment switch - { - 1 => LayoutOptions.Start, - 2 => LayoutOptions.Center, - 3 => LayoutOptions.End, - 0 => LayoutOptions.Fill, - _ => LayoutOptions.Start, - }); - } - } + public static void MapLineBreakMode(LabelHandler handler, ILabel label) + { + if (handler.PlatformView is null) return; - public static void MapHorizontalLayoutAlignment(LabelHandler handler, ILabel label) - { - //IL_0011: Unknown result type (might be due to invalid IL or missing references) - //IL_0016: Unknown result type (might be due to invalid IL or missing references) - //IL_0017: Unknown result type (might be due to invalid IL or missing references) - //IL_002d: Expected I4, but got Unknown - //IL_0047: Unknown result type (might be due to invalid IL or missing references) - //IL_004c: Unknown result type (might be due to invalid IL or missing references) - //IL_002f: Unknown result type (might be due to invalid IL or missing references) - //IL_0034: Unknown result type (might be due to invalid IL or missing references) - //IL_0037: Unknown result type (might be due to invalid IL or missing references) - //IL_003c: Unknown result type (might be due to invalid IL or missing references) - //IL_003f: Unknown result type (might be due to invalid IL or missing references) - //IL_0044: Unknown result type (might be due to invalid IL or missing references) - //IL_0056: Unknown result type (might be due to invalid IL or missing references) - //IL_004f: Unknown result type (might be due to invalid IL or missing references) - //IL_0054: Unknown result type (might be due to invalid IL or missing references) - if (((ViewHandler)(object)handler).PlatformView != null) - { - SkiaLabel platformView = ((ViewHandler)(object)handler).PlatformView; - LayoutAlignment horizontalLayoutAlignment = ((IView)label).HorizontalLayoutAlignment; - platformView.HorizontalOptions = (LayoutOptions)((int)horizontalLayoutAlignment switch - { - 1 => LayoutOptions.Start, - 2 => LayoutOptions.Center, - 3 => LayoutOptions.End, - 0 => LayoutOptions.Fill, - _ => LayoutOptions.Start, - }); - } - } + // LineBreakMode is on Label control, not ILabel interface + if (label is Microsoft.Maui.Controls.Label mauiLabel) + { + handler.PlatformView.LineBreakMode = mauiLabel.LineBreakMode switch + { + Microsoft.Maui.LineBreakMode.NoWrap => Platform.LineBreakMode.NoWrap, + Microsoft.Maui.LineBreakMode.WordWrap => Platform.LineBreakMode.WordWrap, + Microsoft.Maui.LineBreakMode.CharacterWrap => Platform.LineBreakMode.CharacterWrap, + Microsoft.Maui.LineBreakMode.HeadTruncation => Platform.LineBreakMode.HeadTruncation, + Microsoft.Maui.LineBreakMode.TailTruncation => Platform.LineBreakMode.TailTruncation, + Microsoft.Maui.LineBreakMode.MiddleTruncation => Platform.LineBreakMode.MiddleTruncation, + _ => Platform.LineBreakMode.TailTruncation + }; + } + } - public static void MapFormattedText(LabelHandler handler, ILabel label) - { - //IL_0081: Unknown result type (might be due to invalid IL or missing references) - //IL_009e: Unknown result type (might be due to invalid IL or missing references) - //IL_00bb: Unknown result type (might be due to invalid IL or missing references) - //IL_00c1: Unknown result type (might be due to invalid IL or missing references) - //IL_00c3: Invalid comparison between Unknown and I4 - //IL_00cd: Unknown result type (might be due to invalid IL or missing references) - //IL_00d3: Unknown result type (might be due to invalid IL or missing references) - //IL_00d5: Invalid comparison between Unknown and I4 - //IL_010c: Unknown result type (might be due to invalid IL or missing references) - //IL_012d: Unknown result type (might be due to invalid IL or missing references) - if (((ViewHandler)(object)handler).PlatformView == null) - { - return; - } - Label val = (Label)(object)((label is Label) ? label : null); - if (val == null) - { - ((ViewHandler)(object)handler).PlatformView.FormattedSpans = null; - return; - } - FormattedString formattedText = val.FormattedText; - if (formattedText == null || formattedText.Spans.Count == 0) - { - ((ViewHandler)(object)handler).PlatformView.FormattedSpans = null; - return; - } - List list = new List(); - foreach (Span span in formattedText.Spans) - { - SkiaTextSpan skiaTextSpan = new SkiaTextSpan - { - Text = (span.Text ?? ""), - IsBold = ((Enum)span.FontAttributes).HasFlag((Enum)(object)(FontAttributes)1), - IsItalic = ((Enum)span.FontAttributes).HasFlag((Enum)(object)(FontAttributes)2), - IsUnderline = ((span.TextDecorations & 1) > 0), - IsStrikethrough = ((span.TextDecorations & 2) > 0), - CharacterSpacing = (float)span.CharacterSpacing, - LineHeight = (float)span.LineHeight - }; - if (span.TextColor != null) - { - skiaTextSpan.TextColor = span.TextColor.ToSKColor(); - } - if (span.BackgroundColor != null) - { - skiaTextSpan.BackgroundColor = span.BackgroundColor.ToSKColor(); - } - if (!string.IsNullOrEmpty(span.FontFamily)) - { - skiaTextSpan.FontFamily = span.FontFamily; - } - if (span.FontSize > 0.0) - { - skiaTextSpan.FontSize = (float)span.FontSize; - } - list.Add(skiaTextSpan); - } - ((ViewHandler)(object)handler).PlatformView.FormattedSpans = list; - } + public static void MapMaxLines(LabelHandler handler, ILabel label) + { + if (handler.PlatformView is null) return; + + // MaxLines is on Label control, not ILabel interface + if (label is Microsoft.Maui.Controls.Label mauiLabel) + { + handler.PlatformView.MaxLines = mauiLabel.MaxLines; + } + } + + public static void MapPadding(LabelHandler handler, ILabel label) + { + if (handler.PlatformView is null) return; + + var padding = label.Padding; + handler.PlatformView.Padding = new SKRect( + (float)padding.Left, + (float)padding.Top, + (float)padding.Right, + (float)padding.Bottom); + } + + public static void MapBackground(LabelHandler handler, ILabel label) + { + if (handler.PlatformView is null) return; + + if (label.Background is SolidPaint solidPaint && solidPaint.Color is not null) + { + handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor(); + } + } + + public static void MapVerticalLayoutAlignment(LabelHandler handler, ILabel label) + { + if (handler.PlatformView is null) return; + + handler.PlatformView.VerticalOptions = label.VerticalLayoutAlignment switch + { + Primitives.LayoutAlignment.Start => LayoutOptions.Start, + Primitives.LayoutAlignment.Center => LayoutOptions.Center, + Primitives.LayoutAlignment.End => LayoutOptions.End, + Primitives.LayoutAlignment.Fill => LayoutOptions.Fill, + _ => LayoutOptions.Start + }; + } + + public static void MapHorizontalLayoutAlignment(LabelHandler handler, ILabel label) + { + if (handler.PlatformView is null) return; + + handler.PlatformView.HorizontalOptions = label.HorizontalLayoutAlignment switch + { + Primitives.LayoutAlignment.Start => LayoutOptions.Start, + Primitives.LayoutAlignment.Center => LayoutOptions.Center, + Primitives.LayoutAlignment.End => LayoutOptions.End, + Primitives.LayoutAlignment.Fill => LayoutOptions.Fill, + _ => LayoutOptions.Start + }; + } } diff --git a/Handlers/LayoutHandler.cs b/Handlers/LayoutHandler.cs index 3ff9544..3b94823 100644 --- a/Handlers/LayoutHandler.cs +++ b/Handlers/LayoutHandler.cs @@ -1,166 +1,368 @@ -using System.Collections.Generic; -using Microsoft.Maui.Controls; -using Microsoft.Maui.Graphics; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using Microsoft.Maui.Handlers; -using Microsoft.Maui.Platform.Linux.Hosting; +using Microsoft.Maui.Graphics; using SkiaSharp; namespace Microsoft.Maui.Platform.Linux.Handlers; -public class LayoutHandler : ViewHandler +/// +/// Handler for Layout on Linux using Skia rendering. +/// Maps ILayout interface to SkiaLayoutView platform view. +/// +public partial class LayoutHandler : ViewHandler { - public static IPropertyMapper Mapper = (IPropertyMapper)(object)new PropertyMapper((IPropertyMapper[])(object)new IPropertyMapper[1] { (IPropertyMapper)ViewHandler.ViewMapper }) - { - ["ClipsToBounds"] = MapClipsToBounds, - ["Background"] = MapBackground, - ["Padding"] = MapPadding - }; + public static IPropertyMapper Mapper = new PropertyMapper(ViewHandler.ViewMapper) + { + [nameof(ILayout.ClipsToBounds)] = MapClipsToBounds, + [nameof(IView.Background)] = MapBackground, + [nameof(IPadding.Padding)] = MapPadding, + }; - public static CommandMapper CommandMapper = new CommandMapper((CommandMapper)(object)ViewHandler.ViewCommandMapper) - { - ["Add"] = MapAdd, - ["Remove"] = MapRemove, - ["Clear"] = MapClear, - ["Insert"] = MapInsert, - ["Update"] = MapUpdate - }; + public static CommandMapper CommandMapper = new(ViewHandler.ViewCommandMapper) + { + ["Add"] = MapAdd, + ["Remove"] = MapRemove, + ["Clear"] = MapClear, + ["Insert"] = MapInsert, + ["Update"] = MapUpdate, + }; - public LayoutHandler() - : base((IPropertyMapper)(object)Mapper, (CommandMapper)(object)CommandMapper) - { - } + public LayoutHandler() : base(Mapper, CommandMapper) + { + } - public LayoutHandler(IPropertyMapper? mapper = null, CommandMapper? commandMapper = null) - : base((IPropertyMapper)(((object)mapper) ?? ((object)Mapper)), (CommandMapper)(((object)commandMapper) ?? ((object)CommandMapper))) - { - } + public LayoutHandler(IPropertyMapper? mapper = null, CommandMapper? commandMapper = null) + : base(mapper ?? Mapper, commandMapper ?? CommandMapper) + { + } - protected override SkiaLayoutView CreatePlatformView() - { - return new SkiaStackLayout(); - } + protected override SkiaLayoutView CreatePlatformView() + { + return new SkiaStackLayout(); + } - protected override void ConnectHandler(SkiaLayoutView platformView) - { - //IL_0036: Unknown result type (might be due to invalid IL or missing references) - base.ConnectHandler(platformView); - if (base.VirtualView == null || ((ElementHandler)this).MauiContext == null) - { - return; - } - ILayout virtualView = base.VirtualView; - VisualElement val = (VisualElement)(object)((virtualView is VisualElement) ? virtualView : null); - if (val != null && val.BackgroundColor != null) - { - platformView.BackgroundColor = val.BackgroundColor.ToSKColor(); - } - for (int i = 0; i < ((ICollection)base.VirtualView).Count; i++) - { - IView val2 = ((IList)base.VirtualView)[i]; - if (val2 != null) - { - if (val2.Handler == null) - { - val2.Handler = val2.ToViewHandler(((ElementHandler)this).MauiContext); - } - IViewHandler handler = val2.Handler; - if (((handler != null) ? ((IElementHandler)handler).PlatformView : null) is SkiaView child) - { - platformView.AddChild(child); - } - } - } - } + protected override void ConnectHandler(SkiaLayoutView platformView) + { + base.ConnectHandler(platformView); - public static void MapClipsToBounds(LayoutHandler handler, ILayout layout) - { - if (((ViewHandler)(object)handler).PlatformView != null) - { - ((ViewHandler)(object)handler).PlatformView.ClipToBounds = layout.ClipsToBounds; - } - } + // Create handlers for all children and add them to the platform view + if (VirtualView == null || MauiContext == null) return; - public static void MapBackground(LayoutHandler handler, ILayout layout) - { - //IL_002c: Unknown result type (might be due to invalid IL or missing references) - if (((ViewHandler)(object)handler).PlatformView != null) - { - Paint background = ((IView)layout).Background; - SolidPaint val = (SolidPaint)(object)((background is SolidPaint) ? background : null); - if (val != null && val.Color != null) - { - ((ViewHandler)(object)handler).PlatformView.BackgroundColor = val.Color.ToSKColor(); - } - } - } + // Explicitly map BackgroundColor since it may be set before handler creation + if (VirtualView is Microsoft.Maui.Controls.VisualElement ve && ve.BackgroundColor != null) + { + platformView.BackgroundColor = ve.BackgroundColor.ToSKColor(); + } - public static void MapAdd(LayoutHandler handler, ILayout layout, object? arg) - { - if (((ViewHandler)(object)handler).PlatformView == null || !(arg is LayoutHandlerUpdate { Index: var index } layoutHandlerUpdate)) - { - return; - } - IView? view = layoutHandlerUpdate.View; - object obj; - if (view == null) - { - obj = null; - } - else - { - IViewHandler handler2 = view.Handler; - obj = ((handler2 != null) ? ((IElementHandler)handler2).PlatformView : null); - } - if (obj is SkiaView child) - { - if (index >= 0 && index < ((ViewHandler)(object)handler).PlatformView.Children.Count) - { - ((ViewHandler)(object)handler).PlatformView.InsertChild(index, child); - } - else - { - ((ViewHandler)(object)handler).PlatformView.AddChild(child); - } - } - } + for (int i = 0; i < VirtualView.Count; i++) + { + var child = VirtualView[i]; + if (child == null) continue; - public static void MapRemove(LayoutHandler handler, ILayout layout, object? arg) - { - if (((ViewHandler)(object)handler).PlatformView != null && arg is LayoutHandlerUpdate { Index: var index } && index >= 0 && index < ((ViewHandler)(object)handler).PlatformView.Children.Count) - { - ((ViewHandler)(object)handler).PlatformView.RemoveChildAt(index); - } - } + // Create handler for child if it doesn't exist + if (child.Handler == null) + { + child.Handler = child.ToHandler(MauiContext); + } - public static void MapClear(LayoutHandler handler, ILayout layout, object? arg) - { - ((ViewHandler)(object)handler).PlatformView?.ClearChildren(); - } + // Add child's platform view to our layout + if (child.Handler?.PlatformView is SkiaView skiaChild) + { + platformView.AddChild(skiaChild); + } + } + } - public static void MapInsert(LayoutHandler handler, ILayout layout, object? arg) - { - MapAdd(handler, layout, arg); - } + public static void MapClipsToBounds(LayoutHandler handler, ILayout layout) + { + if (handler.PlatformView == null) return; + handler.PlatformView.ClipToBounds = layout.ClipsToBounds; + } - public static void MapUpdate(LayoutHandler handler, ILayout layout, object? arg) - { - ((ViewHandler)(object)handler).PlatformView?.InvalidateMeasure(); - } + public static void MapBackground(LayoutHandler handler, ILayout layout) + { + if (handler.PlatformView is null) return; - public static void MapPadding(LayoutHandler handler, ILayout layout) - { - //IL_000f: Unknown result type (might be due to invalid IL or missing references) - //IL_0014: Unknown result type (might be due to invalid IL or missing references) - //IL_003b: Unknown result type (might be due to invalid IL or missing references) - if (((ViewHandler)(object)handler).PlatformView != null) - { - if (layout != null) - { - Thickness padding = ((IPadding)layout).Padding; - ((ViewHandler)(object)handler).PlatformView.Padding = new SKRect((float)((Thickness)(ref padding)).Left, (float)((Thickness)(ref padding)).Top, (float)((Thickness)(ref padding)).Right, (float)((Thickness)(ref padding)).Bottom); - ((ViewHandler)(object)handler).PlatformView.InvalidateMeasure(); - ((ViewHandler)(object)handler).PlatformView.Invalidate(); - } - } - } + if (layout.Background is SolidPaint solidPaint && solidPaint.Color is not null) + { + handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor(); + } + } + + public static void MapAdd(LayoutHandler handler, ILayout layout, object? arg) + { + if (handler.PlatformView == null || arg is not LayoutHandlerUpdate update) + return; + + var index = update.Index; + var child = update.View; + + if (child?.Handler?.PlatformView is SkiaView skiaView) + { + if (index >= 0 && index < handler.PlatformView.Children.Count) + handler.PlatformView.InsertChild(index, skiaView); + else + handler.PlatformView.AddChild(skiaView); + } + } + + public static void MapRemove(LayoutHandler handler, ILayout layout, object? arg) + { + if (handler.PlatformView == null || arg is not LayoutHandlerUpdate update) + return; + + var index = update.Index; + if (index >= 0 && index < handler.PlatformView.Children.Count) + { + handler.PlatformView.RemoveChildAt(index); + } + } + + public static void MapClear(LayoutHandler handler, ILayout layout, object? arg) + { + handler.PlatformView?.ClearChildren(); + } + + public static void MapInsert(LayoutHandler handler, ILayout layout, object? arg) + { + MapAdd(handler, layout, arg); + } + + public static void MapUpdate(LayoutHandler handler, ILayout layout, object? arg) + { + // Force re-layout + handler.PlatformView?.InvalidateMeasure(); + } + + public static void MapPadding(LayoutHandler handler, ILayout layout) + { + if (handler.PlatformView == null) return; + + if (layout is IPadding paddable) + { + var padding = paddable.Padding; + handler.PlatformView.Padding = new SKRect( + (float)padding.Left, + (float)padding.Top, + (float)padding.Right, + (float)padding.Bottom); + handler.PlatformView.InvalidateMeasure(); + handler.PlatformView.Invalidate(); + } + } +} + +/// +/// Update payload for layout changes. +/// +public class LayoutHandlerUpdate +{ + public int Index { get; } + public IView? View { get; } + + public LayoutHandlerUpdate(int index, IView? view) + { + Index = index; + View = view; + } +} + +/// +/// Handler for StackLayout on Linux. +/// +public partial class StackLayoutHandler : LayoutHandler +{ + public static new IPropertyMapper Mapper = new PropertyMapper(LayoutHandler.Mapper) + { + [nameof(IStackLayout.Spacing)] = MapSpacing, + }; + + public StackLayoutHandler() : base(Mapper) + { + } + + protected override SkiaLayoutView CreatePlatformView() + { + return new SkiaStackLayout(); + } + + protected override void ConnectHandler(SkiaLayoutView platformView) + { + // Set orientation first + if (platformView is SkiaStackLayout stackLayout && VirtualView is IStackLayout stackView) + { + // Determine orientation based on view type + if (VirtualView is Microsoft.Maui.Controls.HorizontalStackLayout) + { + stackLayout.Orientation = StackOrientation.Horizontal; + } + else if (VirtualView is Microsoft.Maui.Controls.VerticalStackLayout || + VirtualView is Microsoft.Maui.Controls.StackLayout) + { + stackLayout.Orientation = StackOrientation.Vertical; + } + + stackLayout.Spacing = (float)stackView.Spacing; + } + + // Let base handle children + base.ConnectHandler(platformView); + } + + public static void MapSpacing(StackLayoutHandler handler, IStackLayout layout) + { + if (handler.PlatformView is SkiaStackLayout stackLayout) + { + stackLayout.Spacing = (float)layout.Spacing; + } + } +} + +/// +/// Handler for Grid on Linux. +/// +public partial class GridHandler : LayoutHandler +{ + public static new IPropertyMapper Mapper = new PropertyMapper(LayoutHandler.Mapper) + { + [nameof(IGridLayout.RowSpacing)] = MapRowSpacing, + [nameof(IGridLayout.ColumnSpacing)] = MapColumnSpacing, + [nameof(IGridLayout.RowDefinitions)] = MapRowDefinitions, + [nameof(IGridLayout.ColumnDefinitions)] = MapColumnDefinitions, + }; + + public GridHandler() : base(Mapper) + { + } + + protected override SkiaLayoutView CreatePlatformView() + { + return new SkiaGrid(); + } + + protected override void ConnectHandler(SkiaLayoutView platformView) + { + try + { + // Don't call base - we handle children specially for Grid + if (VirtualView is not IGridLayout gridLayout || MauiContext == null || platformView is not SkiaGrid grid) return; + + Console.WriteLine($"[GridHandler] ConnectHandler: {gridLayout.Count} children, {gridLayout.RowDefinitions.Count} rows, {gridLayout.ColumnDefinitions.Count} cols"); + + // Explicitly map BackgroundColor since it may be set before handler creation + if (VirtualView is Microsoft.Maui.Controls.VisualElement ve && ve.BackgroundColor != null) + { + platformView.BackgroundColor = ve.BackgroundColor.ToSKColor(); + } + + // Explicitly map Padding since it may be set before handler creation + if (VirtualView is IPadding paddable) + { + var padding = paddable.Padding; + platformView.Padding = new SKRect( + (float)padding.Left, + (float)padding.Top, + (float)padding.Right, + (float)padding.Bottom); + Console.WriteLine($"[GridHandler] Applied Padding: L={padding.Left}, T={padding.Top}, R={padding.Right}, B={padding.Bottom}"); + } + + // Map row/column definitions first + MapRowDefinitions(this, gridLayout); + MapColumnDefinitions(this, gridLayout); + + // Add each child with its row/column position + for (int i = 0; i < gridLayout.Count; i++) + { + var child = gridLayout[i]; + if (child == null) continue; + + Console.WriteLine($"[GridHandler] Processing child {i}: {child.GetType().Name}"); + + // Create handler for child if it doesn't exist + if (child.Handler == null) + { + child.Handler = child.ToHandler(MauiContext); + } + + // Get grid position from attached properties + int row = 0, column = 0, rowSpan = 1, columnSpan = 1; + if (child is Microsoft.Maui.Controls.View mauiView) + { + row = Microsoft.Maui.Controls.Grid.GetRow(mauiView); + column = Microsoft.Maui.Controls.Grid.GetColumn(mauiView); + rowSpan = Microsoft.Maui.Controls.Grid.GetRowSpan(mauiView); + columnSpan = Microsoft.Maui.Controls.Grid.GetColumnSpan(mauiView); + } + + Console.WriteLine($"[GridHandler] Child {i} at row={row}, col={column}, handler={child.Handler?.GetType().Name}"); + + // Add child's platform view to our grid + if (child.Handler?.PlatformView is SkiaView skiaChild) + { + grid.AddChild(skiaChild, row, column, rowSpan, columnSpan); + Console.WriteLine($"[GridHandler] Added child {i} to grid"); + } + } + Console.WriteLine($"[GridHandler] ConnectHandler complete"); + } + catch (Exception ex) + { + Console.WriteLine($"[GridHandler] EXCEPTION in ConnectHandler: {ex.GetType().Name}: {ex.Message}"); + Console.WriteLine($"[GridHandler] Stack trace: {ex.StackTrace}"); + throw; + } + } + + public static void MapRowSpacing(GridHandler handler, IGridLayout layout) + { + if (handler.PlatformView is SkiaGrid grid) + { + grid.RowSpacing = (float)layout.RowSpacing; + } + } + + public static void MapColumnSpacing(GridHandler handler, IGridLayout layout) + { + if (handler.PlatformView is SkiaGrid grid) + { + grid.ColumnSpacing = (float)layout.ColumnSpacing; + } + } + + public static void MapRowDefinitions(GridHandler handler, IGridLayout layout) + { + if (handler.PlatformView is not SkiaGrid grid) return; + + grid.RowDefinitions.Clear(); + foreach (var rowDef in layout.RowDefinitions) + { + var height = rowDef.Height; + if (height.IsAbsolute) + grid.RowDefinitions.Add(new Microsoft.Maui.Platform.GridLength((float)height.Value, Microsoft.Maui.Platform.GridUnitType.Absolute)); + else if (height.IsAuto) + grid.RowDefinitions.Add(Microsoft.Maui.Platform.GridLength.Auto); + else // Star + grid.RowDefinitions.Add(new Microsoft.Maui.Platform.GridLength((float)height.Value, Microsoft.Maui.Platform.GridUnitType.Star)); + } + } + + public static void MapColumnDefinitions(GridHandler handler, IGridLayout layout) + { + if (handler.PlatformView is not SkiaGrid grid) return; + + grid.ColumnDefinitions.Clear(); + foreach (var colDef in layout.ColumnDefinitions) + { + var width = colDef.Width; + if (width.IsAbsolute) + grid.ColumnDefinitions.Add(new Microsoft.Maui.Platform.GridLength((float)width.Value, Microsoft.Maui.Platform.GridUnitType.Absolute)); + else if (width.IsAuto) + grid.ColumnDefinitions.Add(Microsoft.Maui.Platform.GridLength.Auto); + else // Star + grid.ColumnDefinitions.Add(new Microsoft.Maui.Platform.GridLength((float)width.Value, Microsoft.Maui.Platform.GridUnitType.Star)); + } + } } diff --git a/Handlers/LayoutHandlerUpdate.cs b/Handlers/LayoutHandlerUpdate.cs deleted file mode 100644 index 8f16c85..0000000 --- a/Handlers/LayoutHandlerUpdate.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Microsoft.Maui.Platform.Linux.Handlers; - -public class LayoutHandlerUpdate -{ - public int Index { get; } - - public IView? View { get; } - - public LayoutHandlerUpdate(int index, IView? view) - { - Index = index; - View = view; - } -} diff --git a/Handlers/LinuxApplicationContext.cs b/Handlers/LinuxApplicationContext.cs deleted file mode 100644 index 2a78e02..0000000 --- a/Handlers/LinuxApplicationContext.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System.Collections.Generic; - -namespace Microsoft.Maui.Platform.Linux.Handlers; - -public class LinuxApplicationContext -{ - private readonly List _windows = new List(); - - private IApplication? _application; - - public IApplication? Application - { - get - { - return _application; - } - set - { - _application = value; - if (_application == null) - { - return; - } - foreach (IWindow window in _application.Windows) - { - if (!_windows.Contains(window)) - { - _windows.Add(window); - } - } - } - } - - public IReadOnlyList Windows => _windows; - - public IWindow? MainWindow - { - get - { - if (_windows.Count <= 0) - { - return null; - } - return _windows[0]; - } - } - - public void OpenWindow(IWindow window) - { - if (!_windows.Contains(window)) - { - _windows.Add(window); - } - } - - public void CloseWindow(IWindow window) - { - _windows.Remove(window); - if (_windows.Count == 0) - { - LinuxApplication.Current?.MainWindow?.Stop(); - } - } -} diff --git a/Handlers/NavigationPageHandler.cs b/Handlers/NavigationPageHandler.cs index 54550a3..8e899a4 100644 --- a/Handlers/NavigationPageHandler.cs +++ b/Handlers/NavigationPageHandler.cs @@ -1,448 +1,381 @@ -using System; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.IO; -using System.Linq; -using Microsoft.Maui.Controls; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using Microsoft.Maui.Handlers; -using Microsoft.Maui.Platform.Linux.Hosting; +using Microsoft.Maui.Graphics; +using Microsoft.Maui.Controls; +using Microsoft.Maui.Platform; using SkiaSharp; -using Svg.Skia; +using System.Collections.Specialized; namespace Microsoft.Maui.Platform.Linux.Handlers; -public class NavigationPageHandler : ViewHandler +/// +/// Handler for NavigationPage on Linux using Skia rendering. +/// +public partial class NavigationPageHandler : ViewHandler { - public static IPropertyMapper Mapper = (IPropertyMapper)(object)new PropertyMapper((IPropertyMapper[])(object)new IPropertyMapper[1] { (IPropertyMapper)ViewHandler.ViewMapper }) - { - ["BarBackgroundColor"] = MapBarBackgroundColor, - ["BarBackground"] = MapBarBackground, - ["BarTextColor"] = MapBarTextColor, - ["Background"] = MapBackground - }; + public static IPropertyMapper Mapper = + new PropertyMapper(ViewHandler.ViewMapper) + { + [nameof(NavigationPage.BarBackgroundColor)] = MapBarBackgroundColor, + [nameof(NavigationPage.BarBackground)] = MapBarBackground, + [nameof(NavigationPage.BarTextColor)] = MapBarTextColor, + [nameof(IView.Background)] = MapBackground, + }; - public static CommandMapper CommandMapper = new CommandMapper((CommandMapper)(object)ViewHandler.ViewCommandMapper) { ["RequestNavigation"] = MapRequestNavigation }; + public static CommandMapper CommandMapper = + new(ViewHandler.ViewCommandMapper) + { + [nameof(IStackNavigationView.RequestNavigation)] = MapRequestNavigation, + }; - private readonly Dictionary _toolbarSubscriptions = new Dictionary(); + public NavigationPageHandler() : base(Mapper, CommandMapper) + { + } - public NavigationPageHandler() - : base((IPropertyMapper)(object)Mapper, (CommandMapper)(object)CommandMapper) - { - } + public NavigationPageHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null) + : base(mapper ?? Mapper, commandMapper ?? CommandMapper) + { + } - public NavigationPageHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null) - : base((IPropertyMapper)(((object)mapper) ?? ((object)Mapper)), (CommandMapper)(((object)commandMapper) ?? ((object)CommandMapper))) - { - } + protected override SkiaNavigationPage CreatePlatformView() + { + return new SkiaNavigationPage(); + } - protected override SkiaNavigationPage CreatePlatformView() - { - return new SkiaNavigationPage(); - } + protected override void ConnectHandler(SkiaNavigationPage platformView) + { + base.ConnectHandler(platformView); + platformView.Pushed += OnPushed; + platformView.Popped += OnPopped; + platformView.PoppedToRoot += OnPoppedToRoot; - protected override void ConnectHandler(SkiaNavigationPage platformView) - { - base.ConnectHandler(platformView); - platformView.Pushed += OnPushed; - platformView.Popped += OnPopped; - platformView.PoppedToRoot += OnPoppedToRoot; - if (base.VirtualView != null) - { - base.VirtualView.Pushed += OnVirtualViewPushed; - base.VirtualView.Popped += OnVirtualViewPopped; - base.VirtualView.PoppedToRoot += OnVirtualViewPoppedToRoot; - SetupNavigationStack(); - } - } + // Subscribe to navigation events from virtual view + if (VirtualView != null) + { + VirtualView.Pushed += OnVirtualViewPushed; + VirtualView.Popped += OnVirtualViewPopped; + VirtualView.PoppedToRoot += OnVirtualViewPoppedToRoot; - protected override void DisconnectHandler(SkiaNavigationPage platformView) - { - platformView.Pushed -= OnPushed; - platformView.Popped -= OnPopped; - platformView.PoppedToRoot -= OnPoppedToRoot; - if (base.VirtualView != null) - { - base.VirtualView.Pushed -= OnVirtualViewPushed; - base.VirtualView.Popped -= OnVirtualViewPopped; - base.VirtualView.PoppedToRoot -= OnVirtualViewPoppedToRoot; - } - base.DisconnectHandler(platformView); - } + // Set up initial navigation stack + SetupNavigationStack(); + } + } - private void SetupNavigationStack() - { - //IL_017a: Unknown result type (might be due to invalid IL or missing references) - //IL_018c: Unknown result type (might be due to invalid IL or missing references) - if (base.VirtualView == null || base.PlatformView == null || ((ElementHandler)this).MauiContext == null) - { - return; - } - List list = ((NavigableElement)base.VirtualView).Navigation.NavigationStack.ToList(); - Console.WriteLine($"[NavigationPageHandler] Setting up {list.Count} pages"); - if (list.Count == 0 && base.VirtualView.CurrentPage != null) - { - Console.WriteLine("[NavigationPageHandler] No pages in stack, using CurrentPage: " + base.VirtualView.CurrentPage.Title); - list.Add(base.VirtualView.CurrentPage); - } - foreach (Page item in list) - { - if (((VisualElement)item).Handler == null) - { - Console.WriteLine("[NavigationPageHandler] Creating handler for: " + item.Title); - ((VisualElement)item).Handler = ((IView)(object)item).ToViewHandler(((ElementHandler)this).MauiContext); - } - Console.WriteLine("[NavigationPageHandler] Page handler type: " + ((object)((VisualElement)item).Handler)?.GetType().Name); - IViewHandler handler = ((VisualElement)item).Handler; - Console.WriteLine("[NavigationPageHandler] Page PlatformView type: " + ((handler == null) ? null : ((IElementHandler)handler).PlatformView?.GetType().Name)); - IViewHandler handler2 = ((VisualElement)item).Handler; - if (((handler2 != null) ? ((IElementHandler)handler2).PlatformView : null) is SkiaPage skiaPage) - { - skiaPage.ShowNavigationBar = true; - skiaPage.TitleBarColor = base.PlatformView.BarBackgroundColor; - skiaPage.TitleTextColor = base.PlatformView.BarTextColor; - skiaPage.Title = item.Title ?? ""; - Console.WriteLine("[NavigationPageHandler] SkiaPage content: " + (((object)skiaPage.Content)?.GetType().Name ?? "null")); - if (skiaPage.Content == null) - { - ContentPage val = (ContentPage)(object)((item is ContentPage) ? item : null); - if (val != null && val.Content != null) - { - Console.WriteLine("[NavigationPageHandler] Content is null, manually creating handler for: " + ((object)val.Content).GetType().Name); - if (((VisualElement)val.Content).Handler == null) - { - ((VisualElement)val.Content).Handler = ((IView)(object)val.Content).ToViewHandler(((ElementHandler)this).MauiContext); - } - IViewHandler handler3 = ((VisualElement)val.Content).Handler; - if (((handler3 != null) ? ((IElementHandler)handler3).PlatformView : null) is SkiaView skiaView) - { - skiaPage.Content = skiaView; - Console.WriteLine("[NavigationPageHandler] Set content to: " + ((object)skiaView).GetType().Name); - } - } - } - MapToolbarItems(skiaPage, item); - if (base.PlatformView.StackDepth == 0) - { - Console.WriteLine("[NavigationPageHandler] Setting root page: " + item.Title); - base.PlatformView.SetRootPage(skiaPage); - } - else - { - Console.WriteLine("[NavigationPageHandler] Pushing page: " + item.Title); - base.PlatformView.Push(skiaPage, animated: false); - } - } - else - { - Console.WriteLine("[NavigationPageHandler] Failed to get SkiaPage for: " + item.Title); - } - } - } + protected override void DisconnectHandler(SkiaNavigationPage platformView) + { + platformView.Pushed -= OnPushed; + platformView.Popped -= OnPopped; + platformView.PoppedToRoot -= OnPoppedToRoot; - private void MapToolbarItems(SkiaPage skiaPage, Page page) - { - //IL_0101: Unknown result type (might be due to invalid IL or missing references) - //IL_0119: Unknown result type (might be due to invalid IL or missing references) - //IL_011f: Invalid comparison between Unknown and I4 - if (!(skiaPage is SkiaContentPage skiaContentPage)) - { - return; - } - Console.WriteLine($"[NavigationPageHandler] MapToolbarItems for '{page.Title}', count={page.ToolbarItems.Count}"); - skiaContentPage.ToolbarItems.Clear(); - foreach (ToolbarItem toolbarItem2 in page.ToolbarItems) - { - Console.WriteLine($"[NavigationPageHandler] Adding toolbar item: '{((MenuItem)toolbarItem2).Text}', IconImageSource={((MenuItem)toolbarItem2).IconImageSource}, Order={toolbarItem2.Order}"); - SkiaToolbarItemOrder order = (((int)toolbarItem2.Order == 2) ? SkiaToolbarItemOrder.Secondary : SkiaToolbarItemOrder.Primary); - ToolbarItem toolbarItem = toolbarItem2; - RelayCommand command = new RelayCommand(delegate - { - Console.WriteLine("[NavigationPageHandler] ToolbarItem '" + ((MenuItem)toolbarItem).Text + "' clicked, invoking..."); - IMenuItemController val2 = (IMenuItemController)(object)toolbarItem; - if (val2 != null) - { - val2.Activate(); - } - else - { - ((MenuItem)toolbarItem).Command?.Execute(((MenuItem)toolbarItem).CommandParameter); - } - }); - SKBitmap icon = null; - ImageSource iconImageSource = ((MenuItem)toolbarItem2).IconImageSource; - FileImageSource val = (FileImageSource)(object)((iconImageSource is FileImageSource) ? iconImageSource : null); - if (val != null && !string.IsNullOrEmpty(val.File)) - { - icon = LoadToolbarIcon(val.File); - } - skiaContentPage.ToolbarItems.Add(new SkiaToolbarItem - { - Text = (((MenuItem)toolbarItem2).Text ?? ""), - Icon = icon, - Order = order, - Command = command - }); - } - if (page.ToolbarItems is INotifyCollectionChanged notifyCollectionChanged && !_toolbarSubscriptions.ContainsKey(page)) - { - Console.WriteLine("[NavigationPageHandler] Subscribing to ToolbarItems changes for '" + page.Title + "'"); - notifyCollectionChanged.CollectionChanged += delegate(object? s, NotifyCollectionChangedEventArgs e) - { - Console.WriteLine($"[NavigationPageHandler] ToolbarItems changed for '{page.Title}', action={e.Action}"); - MapToolbarItems(skiaPage, page); - skiaPage.Invalidate(); - }; - _toolbarSubscriptions[page] = (skiaPage, notifyCollectionChanged); - } - } + if (VirtualView != null) + { + VirtualView.Pushed -= OnVirtualViewPushed; + VirtualView.Popped -= OnVirtualViewPopped; + VirtualView.PoppedToRoot -= OnVirtualViewPoppedToRoot; + } - private SKBitmap? LoadToolbarIcon(string fileName) - { - //IL_00d5: Unknown result type (might be due to invalid IL or missing references) - //IL_00db: Expected O, but got Unknown - //IL_00f4: Unknown result type (might be due to invalid IL or missing references) - //IL_00f9: Unknown result type (might be due to invalid IL or missing references) - //IL_011b: Unknown result type (might be due to invalid IL or missing references) - //IL_0122: Expected O, but got Unknown - //IL_0124: Unknown result type (might be due to invalid IL or missing references) - //IL_012b: Expected O, but got Unknown - //IL_012d: Unknown result type (might be due to invalid IL or missing references) - try - { - string baseDirectory = AppContext.BaseDirectory; - string text = Path.Combine(baseDirectory, fileName); - string text2 = Path.Combine(baseDirectory, Path.ChangeExtension(fileName, ".svg")); - Console.WriteLine("[NavigationPageHandler] LoadToolbarIcon: Looking for " + fileName); - Console.WriteLine($"[NavigationPageHandler] Trying PNG: {text} (exists: {File.Exists(text)})"); - Console.WriteLine($"[NavigationPageHandler] Trying SVG: {text2} (exists: {File.Exists(text2)})"); - if (File.Exists(text2)) - { - SKSvg val = new SKSvg(); - try - { - val.Load(text2); - if (val.Picture != null) - { - SKRect cullRect = val.Picture.CullRect; - float num = 24f / Math.Max(((SKRect)(ref cullRect)).Width, ((SKRect)(ref cullRect)).Height); - SKBitmap val2 = new SKBitmap(24, 24, false); - SKCanvas val3 = new SKCanvas(val2); - try - { - val3.Clear(SKColors.Transparent); - val3.Scale(num); - val3.DrawPicture(val.Picture, (SKPaint)null); - Console.WriteLine("[NavigationPageHandler] Loaded SVG icon: " + text2); - return val2; - } - finally - { - ((IDisposable)val3)?.Dispose(); - } - } - } - finally - { - ((IDisposable)val)?.Dispose(); - } - } - if (File.Exists(text)) - { - using (FileStream fileStream = File.OpenRead(text)) - { - SKBitmap result = SKBitmap.Decode((Stream)fileStream); - Console.WriteLine("[NavigationPageHandler] Loaded PNG icon: " + text); - return result; - } - } - Console.WriteLine("[NavigationPageHandler] Icon not found: " + fileName); - return null; - } - catch (Exception ex) - { - Console.WriteLine("[NavigationPageHandler] Error loading icon " + fileName + ": " + ex.Message); - return null; - } - } + base.DisconnectHandler(platformView); + } - private void OnVirtualViewPushed(object? sender, NavigationEventArgs e) - { - //IL_0111: Unknown result type (might be due to invalid IL or missing references) - //IL_0122: Unknown result type (might be due to invalid IL or missing references) - try - { - Page page = e.Page; - Console.WriteLine("[NavigationPageHandler] VirtualView Pushed: " + ((page != null) ? page.Title : null)); - if (e.Page == null || base.PlatformView == null || ((ElementHandler)this).MauiContext == null) - { - return; - } - if (((VisualElement)e.Page).Handler == null) - { - Console.WriteLine("[NavigationPageHandler] Creating handler for page: " + ((object)e.Page).GetType().Name); - ((VisualElement)e.Page).Handler = ((IView)(object)e.Page).ToViewHandler(((ElementHandler)this).MauiContext); - Console.WriteLine("[NavigationPageHandler] Handler created: " + ((object)((VisualElement)e.Page).Handler)?.GetType().Name); - } - IViewHandler handler = ((VisualElement)e.Page).Handler; - if (((handler != null) ? ((IElementHandler)handler).PlatformView : null) is SkiaPage skiaPage) - { - Console.WriteLine("[NavigationPageHandler] Setting up skiaPage, content: " + (((object)skiaPage.Content)?.GetType().Name ?? "null")); - skiaPage.ShowNavigationBar = true; - skiaPage.TitleBarColor = base.PlatformView.BarBackgroundColor; - skiaPage.TitleTextColor = base.PlatformView.BarTextColor; - skiaPage.Title = e.Page.Title ?? ""; - if (skiaPage.Content == null) - { - Page page2 = e.Page; - ContentPage val = (ContentPage)(object)((page2 is ContentPage) ? page2 : null); - if (val != null && val.Content != null) - { - Console.WriteLine("[NavigationPageHandler] Content is null, creating handler for: " + ((object)val.Content).GetType().Name); - if (((VisualElement)val.Content).Handler == null) - { - ((VisualElement)val.Content).Handler = ((IView)(object)val.Content).ToViewHandler(((ElementHandler)this).MauiContext); - } - IViewHandler handler2 = ((VisualElement)val.Content).Handler; - if (((handler2 != null) ? ((IElementHandler)handler2).PlatformView : null) is SkiaView skiaView) - { - skiaPage.Content = skiaView; - Console.WriteLine("[NavigationPageHandler] Set content to: " + ((object)skiaView).GetType().Name); - } - } - } - Console.WriteLine("[NavigationPageHandler] Mapping toolbar items"); - MapToolbarItems(skiaPage, e.Page); - Console.WriteLine("[NavigationPageHandler] Pushing page to platform"); - base.PlatformView.Push(skiaPage, animated: false); - Console.WriteLine($"[NavigationPageHandler] Push complete, thread={Environment.CurrentManagedThreadId}"); - } - Console.WriteLine("[NavigationPageHandler] OnVirtualViewPushed returning"); - } - catch (Exception ex) - { - Console.WriteLine("[NavigationPageHandler] EXCEPTION in OnVirtualViewPushed: " + ex.GetType().Name + ": " + ex.Message); - Console.WriteLine("[NavigationPageHandler] Stack trace: " + ex.StackTrace); - throw; - } - } + private void SetupNavigationStack() + { + if (VirtualView == null || PlatformView == null || MauiContext == null) return; - private void OnVirtualViewPopped(object? sender, NavigationEventArgs e) - { - Page page = e.Page; - Console.WriteLine("[NavigationPageHandler] VirtualView Popped: " + ((page != null) ? page.Title : null)); - base.PlatformView?.Pop(); - } + // Get all pages in the navigation stack + var pages = VirtualView.Navigation.NavigationStack.ToList(); + Console.WriteLine($"[NavigationPageHandler] Setting up {pages.Count} pages"); - private void OnVirtualViewPoppedToRoot(object? sender, NavigationEventArgs e) - { - Console.WriteLine("[NavigationPageHandler] VirtualView PoppedToRoot"); - base.PlatformView?.PopToRoot(); - } + // If no pages in stack, check CurrentPage + if (pages.Count == 0 && VirtualView.CurrentPage != null) + { + Console.WriteLine($"[NavigationPageHandler] No pages in stack, using CurrentPage: {VirtualView.CurrentPage.Title}"); + pages.Add(VirtualView.CurrentPage); + } - private void OnPushed(object? sender, NavigationEventArgs e) - { - } + foreach (var page in pages) + { + // Ensure the page has a handler + if (page.Handler == null) + { + Console.WriteLine($"[NavigationPageHandler] Creating handler for: {page.Title}"); + page.Handler = page.ToHandler(MauiContext); + } - private void OnPopped(object? sender, NavigationEventArgs e) - { - NavigationPage virtualView = base.VirtualView; - if (virtualView != null && ((NavigableElement)virtualView).Navigation.NavigationStack.Count > 1) - { - ((NavigableElement)base.VirtualView).Navigation.RemovePage(((NavigableElement)base.VirtualView).Navigation.NavigationStack.Last()); - } - } + Console.WriteLine($"[NavigationPageHandler] Page handler type: {page.Handler?.GetType().Name}"); + Console.WriteLine($"[NavigationPageHandler] Page PlatformView type: {page.Handler?.PlatformView?.GetType().Name}"); - private void OnPoppedToRoot(object? sender, NavigationEventArgs e) - { - } + if (page.Handler?.PlatformView is SkiaPage skiaPage) + { + // Set navigation bar properties + skiaPage.ShowNavigationBar = true; + skiaPage.TitleBarColor = PlatformView.BarBackgroundColor; + skiaPage.TitleTextColor = PlatformView.BarTextColor; + skiaPage.Title = page.Title ?? ""; - public static void MapBarBackgroundColor(NavigationPageHandler handler, NavigationPage navigationPage) - { - //IL_001d: Unknown result type (might be due to invalid IL or missing references) - if (((ViewHandler)(object)handler).PlatformView != null && navigationPage.BarBackgroundColor != null) - { - ((ViewHandler)(object)handler).PlatformView.BarBackgroundColor = navigationPage.BarBackgroundColor.ToSKColor(); - } - } + Console.WriteLine($"[NavigationPageHandler] SkiaPage content: {skiaPage.Content?.GetType().Name ?? "null"}"); - public static void MapBarBackground(NavigationPageHandler handler, NavigationPage navigationPage) - { - //IL_0024: Unknown result type (might be due to invalid IL or missing references) - if (((ViewHandler)(object)handler).PlatformView != null) - { - Brush barBackground = navigationPage.BarBackground; - SolidColorBrush val = (SolidColorBrush)(object)((barBackground is SolidColorBrush) ? barBackground : null); - if (val != null) - { - ((ViewHandler)(object)handler).PlatformView.BarBackgroundColor = val.Color.ToSKColor(); - } - } - } + // If content is null, try to get it from ContentPage + if (skiaPage.Content == null && page is ContentPage contentPage && contentPage.Content != null) + { + Console.WriteLine($"[NavigationPageHandler] Content is null, manually creating handler for: {contentPage.Content.GetType().Name}"); + if (contentPage.Content.Handler == null) + { + contentPage.Content.Handler = contentPage.Content.ToHandler(MauiContext); + } + if (contentPage.Content.Handler?.PlatformView is SkiaView skiaContent) + { + skiaPage.Content = skiaContent; + Console.WriteLine($"[NavigationPageHandler] Set content to: {skiaContent.GetType().Name}"); + } + } - public static void MapBarTextColor(NavigationPageHandler handler, NavigationPage navigationPage) - { - //IL_001d: Unknown result type (might be due to invalid IL or missing references) - if (((ViewHandler)(object)handler).PlatformView != null && navigationPage.BarTextColor != null) - { - ((ViewHandler)(object)handler).PlatformView.BarTextColor = navigationPage.BarTextColor.ToSKColor(); - } - } + // Map toolbar items + MapToolbarItems(skiaPage, page); - public static void MapBackground(NavigationPageHandler handler, NavigationPage navigationPage) - { - //IL_0024: Unknown result type (might be due to invalid IL or missing references) - if (((ViewHandler)(object)handler).PlatformView != null) - { - Brush background = ((VisualElement)navigationPage).Background; - SolidColorBrush val = (SolidColorBrush)(object)((background is SolidColorBrush) ? background : null); - if (val != null) - { - ((ViewHandler)(object)handler).PlatformView.BackgroundColor = val.Color.ToSKColor(); - } - } - } + if (PlatformView.StackDepth == 0) + { + Console.WriteLine($"[NavigationPageHandler] Setting root page: {page.Title}"); + PlatformView.SetRootPage(skiaPage); + } + else + { + Console.WriteLine($"[NavigationPageHandler] Pushing page: {page.Title}"); + PlatformView.Push(skiaPage, false); + } + } + else + { + Console.WriteLine($"[NavigationPageHandler] Failed to get SkiaPage for: {page.Title}"); + } + } + } - public static void MapRequestNavigation(NavigationPageHandler handler, NavigationPage navigationPage, object? args) - { - //IL_00c5: Unknown result type (might be due to invalid IL or missing references) - //IL_00d7: Unknown result type (might be due to invalid IL or missing references) - if (((ViewHandler)(object)handler).PlatformView == null || ((ElementHandler)handler).MauiContext == null) - { - return; - } - NavigationRequest val = (NavigationRequest)((args is NavigationRequest) ? args : null); - if (val == null) - { - return; - } - Console.WriteLine($"[NavigationPageHandler] MapRequestNavigation: {val.NavigationStack.Count} pages"); - foreach (IView item in val.NavigationStack) - { - Page val2 = (Page)(object)((item is Page) ? item : null); - if (val2 == null) - { - continue; - } - if (((VisualElement)val2).Handler == null) - { - ((VisualElement)val2).Handler = ((IView)(object)val2).ToViewHandler(((ElementHandler)handler).MauiContext); - } - IViewHandler handler2 = ((VisualElement)val2).Handler; - if (((handler2 != null) ? ((IElementHandler)handler2).PlatformView : null) is SkiaPage skiaPage) - { - skiaPage.ShowNavigationBar = true; - skiaPage.TitleBarColor = ((ViewHandler)(object)handler).PlatformView.BarBackgroundColor; - skiaPage.TitleTextColor = ((ViewHandler)(object)handler).PlatformView.BarTextColor; - handler.MapToolbarItems(skiaPage, val2); - if (((ViewHandler)(object)handler).PlatformView.StackDepth == 0) - { - ((ViewHandler)(object)handler).PlatformView.SetRootPage(skiaPage); - } - else - { - ((ViewHandler)(object)handler).PlatformView.Push(skiaPage, val.Animated); - } - } - } - } + private readonly Dictionary _toolbarSubscriptions = new(); + + private void MapToolbarItems(SkiaPage skiaPage, Page page) + { + if (skiaPage is SkiaContentPage contentPage) + { + Console.WriteLine($"[NavigationPageHandler] MapToolbarItems for '{page.Title}', count={page.ToolbarItems.Count}"); + + contentPage.ToolbarItems.Clear(); + foreach (var item in page.ToolbarItems) + { + Console.WriteLine($"[NavigationPageHandler] Adding toolbar item: '{item.Text}', Order={item.Order}"); + // Default and Primary should both be treated as Primary (shown in toolbar) + // Only Secondary goes to overflow menu + var order = item.Order == ToolbarItemOrder.Secondary + ? SkiaToolbarItemOrder.Secondary + : SkiaToolbarItemOrder.Primary; + + // Create a command that invokes the Clicked event + var toolbarItem = item; // Capture for closure + var clickCommand = new RelayCommand(() => + { + Console.WriteLine($"[NavigationPageHandler] ToolbarItem '{toolbarItem.Text}' clicked, invoking..."); + // Use IMenuItemController to send the click + if (toolbarItem is IMenuItemController menuController) + { + menuController.Activate(); + } + else + { + // Fallback: invoke Command if set + toolbarItem.Command?.Execute(toolbarItem.CommandParameter); + } + }); + + contentPage.ToolbarItems.Add(new SkiaToolbarItem + { + Text = item.Text ?? "", + Order = order, + Command = clickCommand + }); + } + + // Subscribe to ToolbarItems changes if not already subscribed + if (page.ToolbarItems is INotifyCollectionChanged notifyCollection && !_toolbarSubscriptions.ContainsKey(page)) + { + Console.WriteLine($"[NavigationPageHandler] Subscribing to ToolbarItems changes for '{page.Title}'"); + notifyCollection.CollectionChanged += (s, e) => + { + Console.WriteLine($"[NavigationPageHandler] ToolbarItems changed for '{page.Title}', action={e.Action}"); + MapToolbarItems(skiaPage, page); + skiaPage.Invalidate(); + }; + _toolbarSubscriptions[page] = (skiaPage, notifyCollection); + } + } + } + + private void OnVirtualViewPushed(object? sender, Microsoft.Maui.Controls.NavigationEventArgs e) + { + try + { + Console.WriteLine($"[NavigationPageHandler] VirtualView Pushed: {e.Page?.Title}"); + if (e.Page == null || PlatformView == null || MauiContext == null) return; + + // Ensure the page has a handler + if (e.Page.Handler == null) + { + Console.WriteLine($"[NavigationPageHandler] Creating handler for page: {e.Page.GetType().Name}"); + e.Page.Handler = e.Page.ToHandler(MauiContext); + Console.WriteLine($"[NavigationPageHandler] Handler created: {e.Page.Handler?.GetType().Name}"); + } + + if (e.Page.Handler?.PlatformView is SkiaPage skiaPage) + { + Console.WriteLine($"[NavigationPageHandler] Setting up skiaPage, content: {skiaPage.Content?.GetType().Name ?? "null"}"); + skiaPage.ShowNavigationBar = true; + skiaPage.TitleBarColor = PlatformView.BarBackgroundColor; + skiaPage.TitleTextColor = PlatformView.BarTextColor; + Console.WriteLine($"[NavigationPageHandler] Mapping toolbar items"); + MapToolbarItems(skiaPage, e.Page); + Console.WriteLine($"[NavigationPageHandler] Pushing page to platform"); + PlatformView.Push(skiaPage, true); + Console.WriteLine($"[NavigationPageHandler] Push complete"); + } + } + catch (Exception ex) + { + Console.WriteLine($"[NavigationPageHandler] EXCEPTION in OnVirtualViewPushed: {ex.GetType().Name}: {ex.Message}"); + Console.WriteLine($"[NavigationPageHandler] Stack trace: {ex.StackTrace}"); + throw; + } + } + + private void OnVirtualViewPopped(object? sender, Microsoft.Maui.Controls.NavigationEventArgs e) + { + Console.WriteLine($"[NavigationPageHandler] VirtualView Popped: {e.Page?.Title}"); + // Pop on the platform side to sync with MAUI navigation + PlatformView?.Pop(true); + } + + private void OnVirtualViewPoppedToRoot(object? sender, Microsoft.Maui.Controls.NavigationEventArgs e) + { + Console.WriteLine($"[NavigationPageHandler] VirtualView PoppedToRoot"); + PlatformView?.PopToRoot(true); + } + + private void OnPushed(object? sender, NavigationEventArgs e) + { + // Navigation was completed on platform side + } + + private void OnPopped(object? sender, NavigationEventArgs e) + { + // Sync back to virtual view - pop from MAUI navigation stack + if (VirtualView?.Navigation.NavigationStack.Count > 1) + { + // Don't trigger another pop on platform side + VirtualView.Navigation.RemovePage(VirtualView.Navigation.NavigationStack.Last()); + } + } + + private void OnPoppedToRoot(object? sender, NavigationEventArgs e) + { + // Navigation was reset + } + + public static void MapBarBackgroundColor(NavigationPageHandler handler, NavigationPage navigationPage) + { + if (handler.PlatformView is null) return; + + if (navigationPage.BarBackgroundColor is not null) + { + handler.PlatformView.BarBackgroundColor = navigationPage.BarBackgroundColor.ToSKColor(); + } + } + + public static void MapBarBackground(NavigationPageHandler handler, NavigationPage navigationPage) + { + if (handler.PlatformView is null) return; + + if (navigationPage.BarBackground is SolidColorBrush solidBrush) + { + handler.PlatformView.BarBackgroundColor = solidBrush.Color.ToSKColor(); + } + } + + public static void MapBarTextColor(NavigationPageHandler handler, NavigationPage navigationPage) + { + if (handler.PlatformView is null) return; + + if (navigationPage.BarTextColor is not null) + { + handler.PlatformView.BarTextColor = navigationPage.BarTextColor.ToSKColor(); + } + } + + public static void MapBackground(NavigationPageHandler handler, NavigationPage navigationPage) + { + if (handler.PlatformView is null) return; + + if (navigationPage.Background is SolidColorBrush solidBrush) + { + handler.PlatformView.BackgroundColor = solidBrush.Color.ToSKColor(); + } + } + + public static void MapRequestNavigation(NavigationPageHandler handler, NavigationPage navigationPage, object? args) + { + if (handler.PlatformView is null || handler.MauiContext is null || args is not NavigationRequest request) + return; + + Console.WriteLine($"[NavigationPageHandler] MapRequestNavigation: {request.NavigationStack.Count} pages"); + + // Handle navigation request + foreach (var view in request.NavigationStack) + { + if (view is not Page page) continue; + + // Ensure handler exists + if (page.Handler == null) + { + page.Handler = page.ToHandler(handler.MauiContext); + } + + if (page.Handler?.PlatformView is SkiaPage skiaPage) + { + skiaPage.ShowNavigationBar = true; + skiaPage.TitleBarColor = handler.PlatformView.BarBackgroundColor; + skiaPage.TitleTextColor = handler.PlatformView.BarTextColor; + handler.MapToolbarItems(skiaPage, page); + + if (handler.PlatformView.StackDepth == 0) + { + handler.PlatformView.SetRootPage(skiaPage); + } + else + { + handler.PlatformView.Push(skiaPage, request.Animated); + } + } + } + } +} + +/// +/// Simple relay command for invoking actions. +/// +internal class RelayCommand : System.Windows.Input.ICommand +{ + private readonly Action _execute; + private readonly Func? _canExecute; + + public RelayCommand(Action execute, Func? canExecute = null) + { + _execute = execute ?? throw new ArgumentNullException(nameof(execute)); + _canExecute = canExecute; + } + + public event EventHandler? CanExecuteChanged; + + public bool CanExecute(object? parameter) => _canExecute?.Invoke() ?? true; + + public void Execute(object? parameter) => _execute(); + + public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty); } diff --git a/Handlers/PageHandler.cs b/Handlers/PageHandler.cs index b83be01..67e42e7 100644 --- a/Handlers/PageHandler.cs +++ b/Handlers/PageHandler.cs @@ -1,124 +1,166 @@ -using System; -using Microsoft.Maui.Controls; -using Microsoft.Maui.Graphics; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using Microsoft.Maui.Handlers; +using Microsoft.Maui.Graphics; +using Microsoft.Maui.Controls; +using Microsoft.Maui.Platform; +using SkiaSharp; namespace Microsoft.Maui.Platform.Linux.Handlers; -public class PageHandler : ViewHandler +/// +/// Base handler for Page on Linux using Skia rendering. +/// +public partial class PageHandler : ViewHandler { - public static IPropertyMapper Mapper = (IPropertyMapper)(object)new PropertyMapper((IPropertyMapper[])(object)new IPropertyMapper[1] { (IPropertyMapper)ViewHandler.ViewMapper }) - { - ["Title"] = MapTitle, - ["BackgroundImageSource"] = MapBackgroundImageSource, - ["Padding"] = MapPadding, - ["Background"] = MapBackground, - ["BackgroundColor"] = MapBackgroundColor - }; + public static IPropertyMapper Mapper = + new PropertyMapper(ViewHandler.ViewMapper) + { + [nameof(Page.Title)] = MapTitle, + [nameof(Page.BackgroundImageSource)] = MapBackgroundImageSource, + [nameof(Page.Padding)] = MapPadding, + [nameof(IView.Background)] = MapBackground, + }; - public static CommandMapper CommandMapper = new CommandMapper((CommandMapper)(object)ViewHandler.ViewCommandMapper); + public static CommandMapper CommandMapper = + new(ViewHandler.ViewCommandMapper) + { + }; - public PageHandler() - : base((IPropertyMapper)(object)Mapper, (CommandMapper)(object)CommandMapper) - { - } + public PageHandler() : base(Mapper, CommandMapper) + { + } - public PageHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null) - : base((IPropertyMapper)(((object)mapper) ?? ((object)Mapper)), (CommandMapper)(((object)commandMapper) ?? ((object)CommandMapper))) - { - } + public PageHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null) + : base(mapper ?? Mapper, commandMapper ?? CommandMapper) + { + } - protected override SkiaPage CreatePlatformView() - { - return new SkiaPage(); - } + protected override SkiaPage CreatePlatformView() + { + return new SkiaPage(); + } - protected override void ConnectHandler(SkiaPage platformView) - { - base.ConnectHandler(platformView); - platformView.Appearing += OnAppearing; - platformView.Disappearing += OnDisappearing; - } + protected override void ConnectHandler(SkiaPage platformView) + { + base.ConnectHandler(platformView); + platformView.Appearing += OnAppearing; + platformView.Disappearing += OnDisappearing; + } - protected override void DisconnectHandler(SkiaPage platformView) - { - platformView.Appearing -= OnAppearing; - platformView.Disappearing -= OnDisappearing; - base.DisconnectHandler(platformView); - } + protected override void DisconnectHandler(SkiaPage platformView) + { + platformView.Appearing -= OnAppearing; + platformView.Disappearing -= OnDisappearing; + base.DisconnectHandler(platformView); + } - private void OnAppearing(object? sender, EventArgs e) - { - Page virtualView = base.VirtualView; - Console.WriteLine("[PageHandler] OnAppearing received for: " + ((virtualView != null) ? virtualView.Title : null)); - Page virtualView2 = base.VirtualView; - if (virtualView2 != null) - { - ((IPageController)virtualView2).SendAppearing(); - } - } + private void OnAppearing(object? sender, EventArgs e) + { + Console.WriteLine($"[PageHandler] OnAppearing received for: {VirtualView?.Title}"); + (VirtualView as IPageController)?.SendAppearing(); + } - private void OnDisappearing(object? sender, EventArgs e) - { - Page virtualView = base.VirtualView; - if (virtualView != null) - { - ((IPageController)virtualView).SendDisappearing(); - } - } + private void OnDisappearing(object? sender, EventArgs e) + { + (VirtualView as IPageController)?.SendDisappearing(); + } - public static void MapTitle(PageHandler handler, Page page) - { - if (((ViewHandler)(object)handler).PlatformView != null) - { - ((ViewHandler)(object)handler).PlatformView.Title = page.Title ?? ""; - } - } + public static void MapTitle(PageHandler handler, Page page) + { + if (handler.PlatformView is null) return; + handler.PlatformView.Title = page.Title ?? ""; + } - public static void MapBackgroundImageSource(PageHandler handler, Page page) - { - ((ViewHandler)(object)handler).PlatformView?.Invalidate(); - } + public static void MapBackgroundImageSource(PageHandler handler, Page page) + { + // Background image would be loaded and set here + // For now, we just invalidate + handler.PlatformView?.Invalidate(); + } - public static void MapPadding(PageHandler handler, Page page) - { - //IL_000a: Unknown result type (might be due to invalid IL or missing references) - //IL_000f: Unknown result type (might be due to invalid IL or missing references) - if (((ViewHandler)(object)handler).PlatformView != null) - { - Thickness padding = page.Padding; - ((ViewHandler)(object)handler).PlatformView.PaddingLeft = (float)((Thickness)(ref padding)).Left; - ((ViewHandler)(object)handler).PlatformView.PaddingTop = (float)((Thickness)(ref padding)).Top; - ((ViewHandler)(object)handler).PlatformView.PaddingRight = (float)((Thickness)(ref padding)).Right; - ((ViewHandler)(object)handler).PlatformView.PaddingBottom = (float)((Thickness)(ref padding)).Bottom; - } - } + public static void MapPadding(PageHandler handler, Page page) + { + if (handler.PlatformView is null) return; - public static void MapBackground(PageHandler handler, Page page) - { - //IL_0024: Unknown result type (might be due to invalid IL or missing references) - if (((ViewHandler)(object)handler).PlatformView != null) - { - Brush background = ((VisualElement)page).Background; - SolidColorBrush val = (SolidColorBrush)(object)((background is SolidColorBrush) ? background : null); - if (val != null) - { - ((ViewHandler)(object)handler).PlatformView.BackgroundColor = val.Color.ToSKColor(); - } - } - } + var padding = page.Padding; + handler.PlatformView.PaddingLeft = (float)padding.Left; + handler.PlatformView.PaddingTop = (float)padding.Top; + handler.PlatformView.PaddingRight = (float)padding.Right; + handler.PlatformView.PaddingBottom = (float)padding.Bottom; + } - public static void MapBackgroundColor(PageHandler handler, Page page) - { - //IL_0022: Unknown result type (might be due to invalid IL or missing references) - if (((ViewHandler)(object)handler).PlatformView != null) - { - Color backgroundColor = ((VisualElement)page).BackgroundColor; - if (backgroundColor != null && backgroundColor != Colors.Transparent) - { - ((ViewHandler)(object)handler).PlatformView.BackgroundColor = backgroundColor.ToSKColor(); - Console.WriteLine($"[PageHandler] MapBackgroundColor: {backgroundColor}"); - } - } - } + public static void MapBackground(PageHandler handler, Page page) + { + if (handler.PlatformView is null) return; + + if (page.Background is SolidColorBrush solidBrush) + { + handler.PlatformView.BackgroundColor = solidBrush.Color.ToSKColor(); + } + } +} + +/// +/// Handler for ContentPage on Linux using Skia rendering. +/// +public partial class ContentPageHandler : PageHandler +{ + public static new IPropertyMapper Mapper = + new PropertyMapper(PageHandler.Mapper) + { + [nameof(ContentPage.Content)] = MapContent, + }; + + public static new CommandMapper CommandMapper = + new(PageHandler.CommandMapper) + { + }; + + public ContentPageHandler() : base(Mapper, CommandMapper) + { + } + + public ContentPageHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null) + : base(mapper ?? Mapper, commandMapper ?? CommandMapper) + { + } + + protected override SkiaPage CreatePlatformView() + { + return new SkiaContentPage(); + } + + public static void MapContent(ContentPageHandler handler, ContentPage page) + { + if (handler.PlatformView is null || handler.MauiContext is null) return; + + // Get the platform view for the content + var content = page.Content; + if (content != null) + { + // Create handler for content if it doesn't exist + if (content.Handler == null) + { + Console.WriteLine($"[ContentPageHandler] Creating handler for content: {content.GetType().Name}"); + content.Handler = content.ToHandler(handler.MauiContext); + } + + // The content's handler should provide the platform view + if (content.Handler?.PlatformView is SkiaView skiaContent) + { + Console.WriteLine($"[ContentPageHandler] Setting content: {skiaContent.GetType().Name}"); + handler.PlatformView.Content = skiaContent; + } + else + { + Console.WriteLine($"[ContentPageHandler] Content handler PlatformView is not SkiaView: {content.Handler?.PlatformView?.GetType().Name ?? "null"}"); + } + } + else + { + handler.PlatformView.Content = null; + } + } } diff --git a/Handlers/PickerHandler.cs b/Handlers/PickerHandler.cs index 273e915..2c015be 100644 --- a/Handlers/PickerHandler.cs +++ b/Handlers/PickerHandler.cs @@ -1,175 +1,161 @@ -using System; -using System.Collections.Specialized; -using System.Linq; -using Microsoft.Maui.Controls; -using Microsoft.Maui.Graphics; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using Microsoft.Maui.Handlers; +using Microsoft.Maui.Graphics; +using Microsoft.Maui.Controls; +using Microsoft.Maui.Platform; +using SkiaSharp; +using System.Collections.Specialized; namespace Microsoft.Maui.Platform.Linux.Handlers; -public class PickerHandler : ViewHandler +/// +/// Handler for Picker on Linux using Skia rendering. +/// +public partial class PickerHandler : ViewHandler { - public static IPropertyMapper Mapper = (IPropertyMapper)(object)new PropertyMapper((IPropertyMapper[])(object)new IPropertyMapper[1] { (IPropertyMapper)ViewHandler.ViewMapper }) - { - ["Title"] = MapTitle, - ["TitleColor"] = MapTitleColor, - ["SelectedIndex"] = MapSelectedIndex, - ["TextColor"] = MapTextColor, - ["Font"] = MapFont, - ["CharacterSpacing"] = MapCharacterSpacing, - ["HorizontalTextAlignment"] = MapHorizontalTextAlignment, - ["VerticalTextAlignment"] = MapVerticalTextAlignment, - ["Background"] = MapBackground, - ["ItemsSource"] = MapItemsSource - }; + public static IPropertyMapper Mapper = + new PropertyMapper(ViewHandler.ViewMapper) + { + [nameof(IPicker.Title)] = MapTitle, + [nameof(IPicker.TitleColor)] = MapTitleColor, + [nameof(IPicker.SelectedIndex)] = MapSelectedIndex, + [nameof(IPicker.TextColor)] = MapTextColor, + [nameof(IPicker.CharacterSpacing)] = MapCharacterSpacing, + [nameof(IPicker.HorizontalTextAlignment)] = MapHorizontalTextAlignment, + [nameof(IPicker.VerticalTextAlignment)] = MapVerticalTextAlignment, + [nameof(IView.Background)] = MapBackground, + [nameof(Picker.ItemsSource)] = MapItemsSource, + }; - public static CommandMapper CommandMapper = new CommandMapper((CommandMapper)(object)ViewHandler.ViewCommandMapper); + public static CommandMapper CommandMapper = + new(ViewHandler.ViewCommandMapper) + { + }; - private INotifyCollectionChanged? _itemsCollection; + private INotifyCollectionChanged? _itemsCollection; - public PickerHandler() - : base((IPropertyMapper)(object)Mapper, (CommandMapper)(object)CommandMapper) - { - } + public PickerHandler() : base(Mapper, CommandMapper) + { + } - public PickerHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null) - : base((IPropertyMapper)(((object)mapper) ?? ((object)Mapper)), (CommandMapper)(((object)commandMapper) ?? ((object)CommandMapper))) - { - } + public PickerHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null) + : base(mapper ?? Mapper, commandMapper ?? CommandMapper) + { + } - protected override SkiaPicker CreatePlatformView() - { - return new SkiaPicker(); - } + protected override SkiaPicker CreatePlatformView() + { + return new SkiaPicker(); + } - protected override void ConnectHandler(SkiaPicker platformView) - { - base.ConnectHandler(platformView); - platformView.SelectedIndexChanged += OnSelectedIndexChanged; - IPicker virtualView = base.VirtualView; - Picker val = (Picker)(object)((virtualView is Picker) ? virtualView : null); - if (val != null && val.Items is INotifyCollectionChanged itemsCollection) - { - _itemsCollection = itemsCollection; - _itemsCollection.CollectionChanged += OnItemsCollectionChanged; - } - ReloadItems(); - } + protected override void ConnectHandler(SkiaPicker platformView) + { + base.ConnectHandler(platformView); + platformView.SelectedIndexChanged += OnSelectedIndexChanged; - protected override void DisconnectHandler(SkiaPicker platformView) - { - platformView.SelectedIndexChanged -= OnSelectedIndexChanged; - if (_itemsCollection != null) - { - _itemsCollection.CollectionChanged -= OnItemsCollectionChanged; - _itemsCollection = null; - } - base.DisconnectHandler(platformView); - } + // Subscribe to items collection changes + if (VirtualView is Picker picker && picker.Items is INotifyCollectionChanged items) + { + _itemsCollection = items; + _itemsCollection.CollectionChanged += OnItemsCollectionChanged; + } - private void OnItemsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) - { - ReloadItems(); - } + // Load items + ReloadItems(); + } - private void OnSelectedIndexChanged(object? sender, EventArgs e) - { - if (base.VirtualView != null && base.PlatformView != null) - { - base.VirtualView.SelectedIndex = base.PlatformView.SelectedIndex; - } - } + protected override void DisconnectHandler(SkiaPicker platformView) + { + platformView.SelectedIndexChanged -= OnSelectedIndexChanged; - private void ReloadItems() - { - if (base.PlatformView != null && base.VirtualView != null) - { - string[] itemsAsArray = IPickerExtension.GetItemsAsArray(base.VirtualView); - base.PlatformView.SetItems(itemsAsArray.Select((string i) => i?.ToString() ?? "")); - } - } + if (_itemsCollection != null) + { + _itemsCollection.CollectionChanged -= OnItemsCollectionChanged; + _itemsCollection = null; + } - public static void MapTitle(PickerHandler handler, IPicker picker) - { - if (((ViewHandler)(object)handler).PlatformView != null) - { - ((ViewHandler)(object)handler).PlatformView.Title = picker.Title ?? ""; - } - } + base.DisconnectHandler(platformView); + } - public static void MapTitleColor(PickerHandler handler, IPicker picker) - { - //IL_001d: Unknown result type (might be due to invalid IL or missing references) - if (((ViewHandler)(object)handler).PlatformView != null && picker.TitleColor != null) - { - ((ViewHandler)(object)handler).PlatformView.TitleColor = picker.TitleColor.ToSKColor(); - } - } + private void OnItemsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + ReloadItems(); + } - public static void MapSelectedIndex(PickerHandler handler, IPicker picker) - { - if (((ViewHandler)(object)handler).PlatformView != null) - { - ((ViewHandler)(object)handler).PlatformView.SelectedIndex = picker.SelectedIndex; - } - } + private void OnSelectedIndexChanged(object? sender, EventArgs e) + { + if (VirtualView is null || PlatformView is null) return; - public static void MapTextColor(PickerHandler handler, IPicker picker) - { - //IL_001d: Unknown result type (might be due to invalid IL or missing references) - if (((ViewHandler)(object)handler).PlatformView != null && ((ITextStyle)picker).TextColor != null) - { - ((ViewHandler)(object)handler).PlatformView.TextColor = ((ITextStyle)picker).TextColor.ToSKColor(); - } - } + VirtualView.SelectedIndex = PlatformView.SelectedIndex; + } - public static void MapFont(PickerHandler handler, IPicker picker) - { - //IL_000a: Unknown result type (might be due to invalid IL or missing references) - //IL_000f: Unknown result type (might be due to invalid IL or missing references) - if (((ViewHandler)(object)handler).PlatformView != null) - { - Font font = ((ITextStyle)picker).Font; - if (!string.IsNullOrEmpty(((Font)(ref font)).Family)) - { - ((ViewHandler)(object)handler).PlatformView.FontFamily = ((Font)(ref font)).Family; - } - if (((Font)(ref font)).Size > 0.0) - { - ((ViewHandler)(object)handler).PlatformView.FontSize = (float)((Font)(ref font)).Size; - } - ((ViewHandler)(object)handler).PlatformView.Invalidate(); - } - } + private void ReloadItems() + { + if (PlatformView is null || VirtualView is null) return; - public static void MapCharacterSpacing(PickerHandler handler, IPicker picker) - { - } + var items = VirtualView.GetItemsAsArray(); + PlatformView.SetItems(items.Select(i => i?.ToString() ?? "")); + } - public static void MapHorizontalTextAlignment(PickerHandler handler, IPicker picker) - { - } + public static void MapTitle(PickerHandler handler, IPicker picker) + { + if (handler.PlatformView is null) return; + handler.PlatformView.Title = picker.Title ?? ""; + } - public static void MapVerticalTextAlignment(PickerHandler handler, IPicker picker) - { - } + public static void MapTitleColor(PickerHandler handler, IPicker picker) + { + if (handler.PlatformView is null) return; + if (picker.TitleColor is not null) + { + handler.PlatformView.TitleColor = picker.TitleColor.ToSKColor(); + } + } - public static void MapBackground(PickerHandler handler, IPicker picker) - { - //IL_002c: Unknown result type (might be due to invalid IL or missing references) - if (((ViewHandler)(object)handler).PlatformView != null) - { - Paint background = ((IView)picker).Background; - SolidPaint val = (SolidPaint)(object)((background is SolidPaint) ? background : null); - if (val != null && val.Color != null) - { - ((ViewHandler)(object)handler).PlatformView.BackgroundColor = val.Color.ToSKColor(); - } - } - } + public static void MapSelectedIndex(PickerHandler handler, IPicker picker) + { + if (handler.PlatformView is null) return; + handler.PlatformView.SelectedIndex = picker.SelectedIndex; + } - public static void MapItemsSource(PickerHandler handler, IPicker picker) - { - handler.ReloadItems(); - } + public static void MapTextColor(PickerHandler handler, IPicker picker) + { + if (handler.PlatformView is null) return; + if (picker.TextColor is not null) + { + handler.PlatformView.TextColor = picker.TextColor.ToSKColor(); + } + } + + public static void MapCharacterSpacing(PickerHandler handler, IPicker picker) + { + // Character spacing could be implemented with custom text rendering + } + + public static void MapHorizontalTextAlignment(PickerHandler handler, IPicker picker) + { + // Text alignment would require changes to SkiaPicker drawing + } + + public static void MapVerticalTextAlignment(PickerHandler handler, IPicker picker) + { + // Text alignment would require changes to SkiaPicker drawing + } + + public static void MapBackground(PickerHandler handler, IPicker picker) + { + if (handler.PlatformView is null) return; + + if (picker.Background is SolidPaint solidPaint && solidPaint.Color is not null) + { + handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor(); + } + } + + public static void MapItemsSource(PickerHandler handler, IPicker picker) + { + handler.ReloadItems(); + } } diff --git a/Handlers/ProgressBarHandler.cs b/Handlers/ProgressBarHandler.cs index 9e3c1bf..e02ebdc 100644 --- a/Handlers/ProgressBarHandler.cs +++ b/Handlers/ProgressBarHandler.cs @@ -1,113 +1,65 @@ -using System.ComponentModel; -using Microsoft.Maui.Controls; -using Microsoft.Maui.Graphics; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using Microsoft.Maui.Handlers; +using Microsoft.Maui.Graphics; +using SkiaSharp; namespace Microsoft.Maui.Platform.Linux.Handlers; -public class ProgressBarHandler : ViewHandler +/// +/// Handler for ProgressBar on Linux using Skia rendering. +/// Maps IProgress interface to SkiaProgressBar platform view. +/// IProgress has: Progress (0-1), ProgressColor +/// +public partial class ProgressBarHandler : ViewHandler { - public static IPropertyMapper Mapper = (IPropertyMapper)(object)new PropertyMapper((IPropertyMapper[])(object)new IPropertyMapper[1] { (IPropertyMapper)ViewHandler.ViewMapper }) - { - ["Progress"] = MapProgress, - ["ProgressColor"] = MapProgressColor, - ["IsEnabled"] = MapIsEnabled, - ["Background"] = MapBackground, - ["BackgroundColor"] = MapBackgroundColor - }; + public static IPropertyMapper Mapper = new PropertyMapper(ViewHandler.ViewMapper) + { + [nameof(IProgress.Progress)] = MapProgress, + [nameof(IProgress.ProgressColor)] = MapProgressColor, + [nameof(IView.Background)] = MapBackground, + }; - public static CommandMapper CommandMapper = new CommandMapper((CommandMapper)(object)ViewHandler.ViewCommandMapper); + public static CommandMapper CommandMapper = new(ViewHandler.ViewCommandMapper) + { + }; - public ProgressBarHandler() - : base((IPropertyMapper)(object)Mapper, (CommandMapper)(object)CommandMapper) - { - } + public ProgressBarHandler() : base(Mapper, CommandMapper) + { + } - protected override SkiaProgressBar CreatePlatformView() - { - return new SkiaProgressBar(); - } + public ProgressBarHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null) + : base(mapper ?? Mapper, commandMapper ?? CommandMapper) + { + } - protected override void ConnectHandler(SkiaProgressBar platformView) - { - base.ConnectHandler(platformView); - IProgress virtualView = base.VirtualView; - BindableObject val = (BindableObject)(object)((virtualView is BindableObject) ? virtualView : null); - if (val != null) - { - val.PropertyChanged += OnVirtualViewPropertyChanged; - } - IProgress virtualView2 = base.VirtualView; - VisualElement val2 = (VisualElement)(object)((virtualView2 is VisualElement) ? virtualView2 : null); - if (val2 != null) - { - platformView.IsVisible = val2.IsVisible; - } - } + protected override SkiaProgressBar CreatePlatformView() + { + return new SkiaProgressBar(); + } - protected override void DisconnectHandler(SkiaProgressBar platformView) - { - IProgress virtualView = base.VirtualView; - BindableObject val = (BindableObject)(object)((virtualView is BindableObject) ? virtualView : null); - if (val != null) - { - val.PropertyChanged -= OnVirtualViewPropertyChanged; - } - base.DisconnectHandler(platformView); - } + public static void MapProgress(ProgressBarHandler handler, IProgress progress) + { + if (handler.PlatformView is null) return; + handler.PlatformView.Progress = Math.Clamp(progress.Progress, 0.0, 1.0); + } - private void OnVirtualViewPropertyChanged(object? sender, PropertyChangedEventArgs e) - { - IProgress virtualView = base.VirtualView; - VisualElement val = (VisualElement)(object)((virtualView is VisualElement) ? virtualView : null); - if (val != null && e.PropertyName == "IsVisible") - { - base.PlatformView.IsVisible = val.IsVisible; - base.PlatformView.Invalidate(); - } - } + public static void MapProgressColor(ProgressBarHandler handler, IProgress progress) + { + if (handler.PlatformView is null) return; - public static void MapProgress(ProgressBarHandler handler, IProgress progress) - { - ((ViewHandler)(object)handler).PlatformView.Progress = progress.Progress; - } + if (progress.ProgressColor is not null) + handler.PlatformView.ProgressColor = progress.ProgressColor.ToSKColor(); + } - public static void MapProgressColor(ProgressBarHandler handler, IProgress progress) - { - //IL_0014: Unknown result type (might be due to invalid IL or missing references) - if (progress.ProgressColor != null) - { - ((ViewHandler)(object)handler).PlatformView.ProgressColor = progress.ProgressColor.ToSKColor(); - } - ((ViewHandler)(object)handler).PlatformView.Invalidate(); - } + public static void MapBackground(ProgressBarHandler handler, IProgress progress) + { + if (handler.PlatformView is null) return; - public static void MapIsEnabled(ProgressBarHandler handler, IProgress progress) - { - ((ViewHandler)(object)handler).PlatformView.IsEnabled = ((IView)progress).IsEnabled; - ((ViewHandler)(object)handler).PlatformView.Invalidate(); - } - - public static void MapBackground(ProgressBarHandler handler, IProgress progress) - { - //IL_0023: Unknown result type (might be due to invalid IL or missing references) - Paint background = ((IView)progress).Background; - SolidPaint val = (SolidPaint)(object)((background is SolidPaint) ? background : null); - if (val != null && val.Color != null) - { - ((ViewHandler)(object)handler).PlatformView.BackgroundColor = val.Color.ToSKColor(); - ((ViewHandler)(object)handler).PlatformView.Invalidate(); - } - } - - public static void MapBackgroundColor(ProgressBarHandler handler, IProgress progress) - { - //IL_001e: Unknown result type (might be due to invalid IL or missing references) - VisualElement val = (VisualElement)(object)((progress is VisualElement) ? progress : null); - if (val != null && val.BackgroundColor != null) - { - ((ViewHandler)(object)handler).PlatformView.BackgroundColor = val.BackgroundColor.ToSKColor(); - ((ViewHandler)(object)handler).PlatformView.Invalidate(); - } - } + if (progress.Background is SolidPaint solidPaint && solidPaint.Color is not null) + { + handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor(); + } + } } diff --git a/Handlers/RadioButtonHandler.cs b/Handlers/RadioButtonHandler.cs index d192fe5..9879938 100644 --- a/Handlers/RadioButtonHandler.cs +++ b/Handlers/RadioButtonHandler.cs @@ -1,111 +1,106 @@ -using System; -using Microsoft.Maui.Controls; -using Microsoft.Maui.Graphics; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using Microsoft.Maui.Handlers; +using Microsoft.Maui.Graphics; +using Microsoft.Maui.Controls; +using Microsoft.Maui.Platform; +using SkiaSharp; namespace Microsoft.Maui.Platform.Linux.Handlers; -public class RadioButtonHandler : ViewHandler +/// +/// Handler for RadioButton on Linux using Skia rendering. +/// +public partial class RadioButtonHandler : ViewHandler { - public static IPropertyMapper Mapper = (IPropertyMapper)(object)new PropertyMapper((IPropertyMapper[])(object)new IPropertyMapper[1] { (IPropertyMapper)ViewHandler.ViewMapper }) - { - ["IsChecked"] = MapIsChecked, - ["TextColor"] = MapTextColor, - ["Font"] = MapFont, - ["Background"] = MapBackground - }; + public static IPropertyMapper Mapper = + new PropertyMapper(ViewHandler.ViewMapper) + { + [nameof(IRadioButton.IsChecked)] = MapIsChecked, + [nameof(ITextStyle.TextColor)] = MapTextColor, + [nameof(ITextStyle.Font)] = MapFont, + [nameof(IView.Background)] = MapBackground, + }; - public static CommandMapper CommandMapper = new CommandMapper((CommandMapper)(object)ViewHandler.ViewCommandMapper); + public static CommandMapper CommandMapper = + new(ViewHandler.ViewCommandMapper) + { + }; - public RadioButtonHandler() - : base((IPropertyMapper)(object)Mapper, (CommandMapper)(object)CommandMapper) - { - } + public RadioButtonHandler() : base(Mapper, CommandMapper) + { + } - public RadioButtonHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null) - : base((IPropertyMapper)(((object)mapper) ?? ((object)Mapper)), (CommandMapper)(((object)commandMapper) ?? ((object)CommandMapper))) - { - } + public RadioButtonHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null) + : base(mapper ?? Mapper, commandMapper ?? CommandMapper) + { + } - protected override SkiaRadioButton CreatePlatformView() - { - return new SkiaRadioButton(); - } + protected override SkiaRadioButton CreatePlatformView() + { + return new SkiaRadioButton(); + } - protected override void ConnectHandler(SkiaRadioButton platformView) - { - base.ConnectHandler(platformView); - platformView.CheckedChanged += OnCheckedChanged; - IRadioButton virtualView = base.VirtualView; - RadioButton val = (RadioButton)(object)((virtualView is RadioButton) ? virtualView : null); - if (val != null) - { - platformView.Content = val.Content?.ToString() ?? ""; - platformView.GroupName = val.GroupName; - platformView.Value = val.Value; - } - } + protected override void ConnectHandler(SkiaRadioButton platformView) + { + base.ConnectHandler(platformView); + platformView.CheckedChanged += OnCheckedChanged; - protected override void DisconnectHandler(SkiaRadioButton platformView) - { - platformView.CheckedChanged -= OnCheckedChanged; - base.DisconnectHandler(platformView); - } + // Set content if available + if (VirtualView is RadioButton rb) + { + platformView.Content = rb.Content?.ToString() ?? ""; + platformView.GroupName = rb.GroupName; + platformView.Value = rb.Value; + } + } - private void OnCheckedChanged(object? sender, EventArgs e) - { - if (base.VirtualView != null && base.PlatformView != null) - { - base.VirtualView.IsChecked = base.PlatformView.IsChecked; - } - } + protected override void DisconnectHandler(SkiaRadioButton platformView) + { + platformView.CheckedChanged -= OnCheckedChanged; + base.DisconnectHandler(platformView); + } - public static void MapIsChecked(RadioButtonHandler handler, IRadioButton radioButton) - { - if (((ViewHandler)(object)handler).PlatformView != null) - { - ((ViewHandler)(object)handler).PlatformView.IsChecked = radioButton.IsChecked; - } - } + private void OnCheckedChanged(object? sender, EventArgs e) + { + if (VirtualView is null || PlatformView is null) return; + VirtualView.IsChecked = PlatformView.IsChecked; + } - public static void MapTextColor(RadioButtonHandler handler, IRadioButton radioButton) - { - //IL_001d: Unknown result type (might be due to invalid IL or missing references) - if (((ViewHandler)(object)handler).PlatformView != null && ((ITextStyle)radioButton).TextColor != null) - { - ((ViewHandler)(object)handler).PlatformView.TextColor = ((ITextStyle)radioButton).TextColor.ToSKColor(); - } - } + public static void MapIsChecked(RadioButtonHandler handler, IRadioButton radioButton) + { + if (handler.PlatformView is null) return; + handler.PlatformView.IsChecked = radioButton.IsChecked; + } - public static void MapFont(RadioButtonHandler handler, IRadioButton radioButton) - { - //IL_000a: Unknown result type (might be due to invalid IL or missing references) - //IL_000f: Unknown result type (might be due to invalid IL or missing references) - //IL_0029: Unknown result type (might be due to invalid IL or missing references) - //IL_002e: Unknown result type (might be due to invalid IL or missing references) - if (((ViewHandler)(object)handler).PlatformView != null) - { - Font font = ((ITextStyle)radioButton).Font; - if (((Font)(ref font)).Size > 0.0) - { - SkiaRadioButton platformView = ((ViewHandler)(object)handler).PlatformView; - font = ((ITextStyle)radioButton).Font; - platformView.FontSize = (float)((Font)(ref font)).Size; - } - } - } + public static void MapTextColor(RadioButtonHandler handler, IRadioButton radioButton) + { + if (handler.PlatformView is null) return; - public static void MapBackground(RadioButtonHandler handler, IRadioButton radioButton) - { - //IL_002c: Unknown result type (might be due to invalid IL or missing references) - if (((ViewHandler)(object)handler).PlatformView != null) - { - Paint background = ((IView)radioButton).Background; - SolidPaint val = (SolidPaint)(object)((background is SolidPaint) ? background : null); - if (val != null && val.Color != null) - { - ((ViewHandler)(object)handler).PlatformView.BackgroundColor = val.Color.ToSKColor(); - } - } - } + if (radioButton.TextColor is not null) + { + handler.PlatformView.TextColor = radioButton.TextColor.ToSKColor(); + } + } + + public static void MapFont(RadioButtonHandler handler, IRadioButton radioButton) + { + if (handler.PlatformView is null) return; + + if (radioButton.Font.Size > 0) + { + handler.PlatformView.FontSize = (float)radioButton.Font.Size; + } + } + + public static void MapBackground(RadioButtonHandler handler, IRadioButton radioButton) + { + if (handler.PlatformView is null) return; + + if (radioButton.Background is SolidPaint solidPaint && solidPaint.Color is not null) + { + handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor(); + } + } } diff --git a/Handlers/RelayCommand.cs b/Handlers/RelayCommand.cs deleted file mode 100644 index 3d98ab0..0000000 --- a/Handlers/RelayCommand.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using System.Windows.Input; - -namespace Microsoft.Maui.Platform.Linux.Handlers; - -internal class RelayCommand : ICommand -{ - private readonly Action _execute; - - private readonly Func? _canExecute; - - public event EventHandler? CanExecuteChanged; - - public RelayCommand(Action execute, Func? canExecute = null) - { - _execute = execute ?? throw new ArgumentNullException("execute"); - _canExecute = canExecute; - } - - public bool CanExecute(object? parameter) - { - return _canExecute?.Invoke() ?? true; - } - - public void Execute(object? parameter) - { - _execute(); - } - - public void RaiseCanExecuteChanged() - { - this.CanExecuteChanged?.Invoke(this, EventArgs.Empty); - } -} diff --git a/Handlers/ScrollViewHandler.cs b/Handlers/ScrollViewHandler.cs index f65b75d..1faf41e 100644 --- a/Handlers/ScrollViewHandler.cs +++ b/Handlers/ScrollViewHandler.cs @@ -1,115 +1,109 @@ -using System; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using Microsoft.Maui.Handlers; -using Microsoft.Maui.Platform.Linux.Hosting; namespace Microsoft.Maui.Platform.Linux.Handlers; -public class ScrollViewHandler : ViewHandler +/// +/// Handler for ScrollView on Linux using SkiaScrollView. +/// +public partial class ScrollViewHandler : ViewHandler { - public static IPropertyMapper Mapper = (IPropertyMapper)(object)new PropertyMapper((IPropertyMapper[])(object)new IPropertyMapper[1] { (IPropertyMapper)ViewHandler.ViewMapper }) - { - ["Content"] = MapContent, - ["HorizontalScrollBarVisibility"] = MapHorizontalScrollBarVisibility, - ["VerticalScrollBarVisibility"] = MapVerticalScrollBarVisibility, - ["Orientation"] = MapOrientation - }; + public static IPropertyMapper Mapper = + new PropertyMapper(ViewMapper) + { + [nameof(IScrollView.Content)] = MapContent, + [nameof(IScrollView.HorizontalScrollBarVisibility)] = MapHorizontalScrollBarVisibility, + [nameof(IScrollView.VerticalScrollBarVisibility)] = MapVerticalScrollBarVisibility, + [nameof(IScrollView.Orientation)] = MapOrientation, + }; - public static CommandMapper CommandMapper = new CommandMapper((CommandMapper)(object)ViewHandler.ViewCommandMapper) { ["RequestScrollTo"] = MapRequestScrollTo }; + public static CommandMapper CommandMapper = + new(ViewCommandMapper) + { + [nameof(IScrollView.RequestScrollTo)] = MapRequestScrollTo + }; - public ScrollViewHandler() - : base((IPropertyMapper)(object)Mapper, (CommandMapper)(object)CommandMapper) - { - } + public ScrollViewHandler() : base(Mapper, CommandMapper) + { + } - public ScrollViewHandler(IPropertyMapper? mapper) - : base((IPropertyMapper)(((object)mapper) ?? ((object)Mapper)), (CommandMapper)(object)CommandMapper) - { - } + public ScrollViewHandler(IPropertyMapper? mapper) + : base(mapper ?? Mapper, CommandMapper) + { + } - protected override SkiaScrollView CreatePlatformView() - { - return new SkiaScrollView(); - } + protected override SkiaScrollView CreatePlatformView() + { + return new SkiaScrollView(); + } - public static void MapContent(ScrollViewHandler handler, IScrollView scrollView) - { - if (((ViewHandler)(object)handler).PlatformView == null || ((ElementHandler)handler).MauiContext == null) - { - return; - } - IView presentedContent = ((IContentView)scrollView).PresentedContent; - if (presentedContent != null) - { - Console.WriteLine("[ScrollViewHandler] MapContent: " + ((object)presentedContent).GetType().Name); - if (presentedContent.Handler == null) - { - presentedContent.Handler = presentedContent.ToViewHandler(((ElementHandler)handler).MauiContext); - } - IViewHandler handler2 = presentedContent.Handler; - if (((handler2 != null) ? ((IElementHandler)handler2).PlatformView : null) is SkiaView skiaView) - { - Console.WriteLine("[ScrollViewHandler] Setting content: " + ((object)skiaView).GetType().Name); - ((ViewHandler)(object)handler).PlatformView.Content = skiaView; - } - } - else - { - ((ViewHandler)(object)handler).PlatformView.Content = null; - } - } + public static void MapContent(ScrollViewHandler handler, IScrollView scrollView) + { + if (handler.PlatformView == null || handler.MauiContext == null) + return; - public static void MapHorizontalScrollBarVisibility(ScrollViewHandler handler, IScrollView scrollView) - { - //IL_0008: Unknown result type (might be due to invalid IL or missing references) - //IL_000d: Unknown result type (might be due to invalid IL or missing references) - //IL_000e: Unknown result type (might be due to invalid IL or missing references) - //IL_0010: Invalid comparison between Unknown and I4 - //IL_0012: Unknown result type (might be due to invalid IL or missing references) - //IL_0014: Invalid comparison between Unknown and I4 - SkiaScrollView platformView = ((ViewHandler)(object)handler).PlatformView; - ScrollBarVisibility horizontalScrollBarVisibility = scrollView.HorizontalScrollBarVisibility; - ScrollBarVisibility horizontalScrollBarVisibility2 = (((int)horizontalScrollBarVisibility == 1) ? ScrollBarVisibility.Always : (((int)horizontalScrollBarVisibility == 2) ? ScrollBarVisibility.Never : ScrollBarVisibility.Default)); - platformView.HorizontalScrollBarVisibility = horizontalScrollBarVisibility2; - } + var content = scrollView.PresentedContent; + if (content != null) + { + Console.WriteLine($"[ScrollViewHandler] MapContent: {content.GetType().Name}"); - public static void MapVerticalScrollBarVisibility(ScrollViewHandler handler, IScrollView scrollView) - { - //IL_0008: Unknown result type (might be due to invalid IL or missing references) - //IL_000d: Unknown result type (might be due to invalid IL or missing references) - //IL_000e: Unknown result type (might be due to invalid IL or missing references) - //IL_0010: Invalid comparison between Unknown and I4 - //IL_0012: Unknown result type (might be due to invalid IL or missing references) - //IL_0014: Invalid comparison between Unknown and I4 - SkiaScrollView platformView = ((ViewHandler)(object)handler).PlatformView; - ScrollBarVisibility verticalScrollBarVisibility = scrollView.VerticalScrollBarVisibility; - ScrollBarVisibility verticalScrollBarVisibility2 = (((int)verticalScrollBarVisibility == 1) ? ScrollBarVisibility.Always : (((int)verticalScrollBarVisibility == 2) ? ScrollBarVisibility.Never : ScrollBarVisibility.Default)); - platformView.VerticalScrollBarVisibility = verticalScrollBarVisibility2; - } + // Create handler for content if it doesn't exist + if (content.Handler == null) + { + content.Handler = content.ToHandler(handler.MauiContext); + } - public static void MapOrientation(ScrollViewHandler handler, IScrollView scrollView) - { - //IL_0008: Unknown result type (might be due to invalid IL or missing references) - //IL_000d: Unknown result type (might be due to invalid IL or missing references) - //IL_000e: Unknown result type (might be due to invalid IL or missing references) - //IL_0010: Unknown result type (might be due to invalid IL or missing references) - //IL_0022: Expected I4, but got Unknown - SkiaScrollView platformView = ((ViewHandler)(object)handler).PlatformView; - ScrollOrientation orientation = scrollView.Orientation; - platformView.Orientation = (orientation - 1) switch - { - 0 => ScrollOrientation.Horizontal, - 1 => ScrollOrientation.Both, - 2 => ScrollOrientation.Neither, - _ => ScrollOrientation.Vertical, - }; - } + if (content.Handler?.PlatformView is SkiaView skiaContent) + { + Console.WriteLine($"[ScrollViewHandler] Setting content: {skiaContent.GetType().Name}"); + handler.PlatformView.Content = skiaContent; + } + } + else + { + handler.PlatformView.Content = null; + } + } - public static void MapRequestScrollTo(ScrollViewHandler handler, IScrollView scrollView, object? args) - { - ScrollToRequest val = (ScrollToRequest)((args is ScrollToRequest) ? args : null); - if (val != null) - { - ((ViewHandler)(object)handler).PlatformView.ScrollTo((float)val.HorizontalOffset, (float)val.VerticalOffset, !val.Instant); - } - } + public static void MapHorizontalScrollBarVisibility(ScrollViewHandler handler, IScrollView scrollView) + { + handler.PlatformView.HorizontalScrollBarVisibility = scrollView.HorizontalScrollBarVisibility switch + { + Microsoft.Maui.ScrollBarVisibility.Always => ScrollBarVisibility.Always, + Microsoft.Maui.ScrollBarVisibility.Never => ScrollBarVisibility.Never, + _ => ScrollBarVisibility.Default + }; + } + + public static void MapVerticalScrollBarVisibility(ScrollViewHandler handler, IScrollView scrollView) + { + handler.PlatformView.VerticalScrollBarVisibility = scrollView.VerticalScrollBarVisibility switch + { + Microsoft.Maui.ScrollBarVisibility.Always => ScrollBarVisibility.Always, + Microsoft.Maui.ScrollBarVisibility.Never => ScrollBarVisibility.Never, + _ => ScrollBarVisibility.Default + }; + } + + public static void MapOrientation(ScrollViewHandler handler, IScrollView scrollView) + { + handler.PlatformView.Orientation = scrollView.Orientation switch + { + Microsoft.Maui.ScrollOrientation.Horizontal => ScrollOrientation.Horizontal, + Microsoft.Maui.ScrollOrientation.Both => ScrollOrientation.Both, + Microsoft.Maui.ScrollOrientation.Neither => ScrollOrientation.Neither, + _ => ScrollOrientation.Vertical + }; + } + + public static void MapRequestScrollTo(ScrollViewHandler handler, IScrollView scrollView, object? args) + { + if (args is ScrollToRequest request) + { + // Instant means no animation, so we pass !Instant for animated parameter + handler.PlatformView.ScrollTo((float)request.HorizontalOffset, (float)request.VerticalOffset, !request.Instant); + } + } } diff --git a/Handlers/SearchBarHandler.cs b/Handlers/SearchBarHandler.cs index 64dbe47..4d1338c 100644 --- a/Handlers/SearchBarHandler.cs +++ b/Handlers/SearchBarHandler.cs @@ -1,142 +1,135 @@ -using System; -using Microsoft.Maui.Graphics; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using Microsoft.Maui.Handlers; +using Microsoft.Maui.Graphics; +using SkiaSharp; namespace Microsoft.Maui.Platform.Linux.Handlers; -public class SearchBarHandler : ViewHandler +/// +/// Handler for SearchBar on Linux using Skia rendering. +/// Maps ISearchBar interface to SkiaSearchBar platform view. +/// +public partial class SearchBarHandler : ViewHandler { - public static IPropertyMapper Mapper = (IPropertyMapper)(object)new PropertyMapper((IPropertyMapper[])(object)new IPropertyMapper[1] { (IPropertyMapper)ViewHandler.ViewMapper }) - { - ["Text"] = MapText, - ["TextColor"] = MapTextColor, - ["Font"] = MapFont, - ["Placeholder"] = MapPlaceholder, - ["PlaceholderColor"] = MapPlaceholderColor, - ["CancelButtonColor"] = MapCancelButtonColor, - ["Background"] = MapBackground - }; + public static IPropertyMapper Mapper = new PropertyMapper(ViewHandler.ViewMapper) + { + [nameof(ITextInput.Text)] = MapText, + [nameof(ITextStyle.TextColor)] = MapTextColor, + [nameof(ITextStyle.Font)] = MapFont, + [nameof(IPlaceholder.Placeholder)] = MapPlaceholder, + [nameof(IPlaceholder.PlaceholderColor)] = MapPlaceholderColor, + [nameof(ISearchBar.CancelButtonColor)] = MapCancelButtonColor, + [nameof(IView.Background)] = MapBackground, + }; - public static CommandMapper CommandMapper = new CommandMapper((CommandMapper)(object)ViewHandler.ViewCommandMapper); + public static CommandMapper CommandMapper = new(ViewHandler.ViewCommandMapper) + { + }; - public SearchBarHandler() - : base((IPropertyMapper)(object)Mapper, (CommandMapper)(object)CommandMapper) - { - } + public SearchBarHandler() : base(Mapper, CommandMapper) + { + } - public SearchBarHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null) - : base((IPropertyMapper)(((object)mapper) ?? ((object)Mapper)), (CommandMapper)(((object)commandMapper) ?? ((object)CommandMapper))) - { - } + public SearchBarHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null) + : base(mapper ?? Mapper, commandMapper ?? CommandMapper) + { + } - protected override SkiaSearchBar CreatePlatformView() - { - return new SkiaSearchBar(); - } + protected override SkiaSearchBar CreatePlatformView() + { + return new SkiaSearchBar(); + } - protected override void ConnectHandler(SkiaSearchBar platformView) - { - base.ConnectHandler(platformView); - platformView.TextChanged += OnTextChanged; - platformView.SearchButtonPressed += OnSearchButtonPressed; - } + protected override void ConnectHandler(SkiaSearchBar platformView) + { + base.ConnectHandler(platformView); + platformView.TextChanged += OnTextChanged; + platformView.SearchButtonPressed += OnSearchButtonPressed; + } - protected override void DisconnectHandler(SkiaSearchBar platformView) - { - platformView.TextChanged -= OnTextChanged; - platformView.SearchButtonPressed -= OnSearchButtonPressed; - base.DisconnectHandler(platformView); - } + protected override void DisconnectHandler(SkiaSearchBar platformView) + { + platformView.TextChanged -= OnTextChanged; + platformView.SearchButtonPressed -= OnSearchButtonPressed; + base.DisconnectHandler(platformView); + } - private void OnTextChanged(object? sender, TextChangedEventArgs e) - { - if (base.VirtualView != null && base.PlatformView != null && ((ITextInput)base.VirtualView).Text != e.NewTextValue) - { - ((ITextInput)base.VirtualView).Text = e.NewTextValue ?? string.Empty; - } - } + private void OnTextChanged(object? sender, TextChangedEventArgs e) + { + if (VirtualView is null || PlatformView is null) return; - private void OnSearchButtonPressed(object? sender, EventArgs e) - { - ISearchBar virtualView = base.VirtualView; - if (virtualView != null) - { - virtualView.SearchButtonPressed(); - } - } + if (VirtualView.Text != e.NewTextValue) + { + VirtualView.Text = e.NewTextValue ?? string.Empty; + } + } - public static void MapText(SearchBarHandler handler, ISearchBar searchBar) - { - if (((ViewHandler)(object)handler).PlatformView != null && ((ViewHandler)(object)handler).PlatformView.Text != ((ITextInput)searchBar).Text) - { - ((ViewHandler)(object)handler).PlatformView.Text = ((ITextInput)searchBar).Text ?? string.Empty; - } - } + private void OnSearchButtonPressed(object? sender, EventArgs e) + { + VirtualView?.SearchButtonPressed(); + } - public static void MapTextColor(SearchBarHandler handler, ISearchBar searchBar) - { - //IL_001d: Unknown result type (might be due to invalid IL or missing references) - if (((ViewHandler)(object)handler).PlatformView != null && ((ITextStyle)searchBar).TextColor != null) - { - ((ViewHandler)(object)handler).PlatformView.TextColor = ((ITextStyle)searchBar).TextColor.ToSKColor(); - } - } + public static void MapText(SearchBarHandler handler, ISearchBar searchBar) + { + if (handler.PlatformView is null) return; - public static void MapFont(SearchBarHandler handler, ISearchBar searchBar) - { - //IL_000a: Unknown result type (might be due to invalid IL or missing references) - //IL_000f: Unknown result type (might be due to invalid IL or missing references) - if (((ViewHandler)(object)handler).PlatformView != null) - { - Font font = ((ITextStyle)searchBar).Font; - if (((Font)(ref font)).Size > 0.0) - { - ((ViewHandler)(object)handler).PlatformView.FontSize = (float)((Font)(ref font)).Size; - } - if (!string.IsNullOrEmpty(((Font)(ref font)).Family)) - { - ((ViewHandler)(object)handler).PlatformView.FontFamily = ((Font)(ref font)).Family; - } - } - } + if (handler.PlatformView.Text != searchBar.Text) + handler.PlatformView.Text = searchBar.Text ?? string.Empty; + } - public static void MapPlaceholder(SearchBarHandler handler, ISearchBar searchBar) - { - if (((ViewHandler)(object)handler).PlatformView != null) - { - ((ViewHandler)(object)handler).PlatformView.Placeholder = ((IPlaceholder)searchBar).Placeholder ?? string.Empty; - } - } + public static void MapTextColor(SearchBarHandler handler, ISearchBar searchBar) + { + if (handler.PlatformView is null) return; - public static void MapPlaceholderColor(SearchBarHandler handler, ISearchBar searchBar) - { - //IL_001d: Unknown result type (might be due to invalid IL or missing references) - if (((ViewHandler)(object)handler).PlatformView != null && ((IPlaceholder)searchBar).PlaceholderColor != null) - { - ((ViewHandler)(object)handler).PlatformView.PlaceholderColor = ((IPlaceholder)searchBar).PlaceholderColor.ToSKColor(); - } - } + if (searchBar.TextColor is not null) + handler.PlatformView.TextColor = searchBar.TextColor.ToSKColor(); + } - public static void MapCancelButtonColor(SearchBarHandler handler, ISearchBar searchBar) - { - //IL_001d: Unknown result type (might be due to invalid IL or missing references) - if (((ViewHandler)(object)handler).PlatformView != null && searchBar.CancelButtonColor != null) - { - ((ViewHandler)(object)handler).PlatformView.ClearButtonColor = searchBar.CancelButtonColor.ToSKColor(); - } - } + public static void MapFont(SearchBarHandler handler, ISearchBar searchBar) + { + if (handler.PlatformView is null) return; - public static void MapBackground(SearchBarHandler handler, ISearchBar searchBar) - { - //IL_002c: Unknown result type (might be due to invalid IL or missing references) - if (((ViewHandler)(object)handler).PlatformView != null) - { - Paint background = ((IView)searchBar).Background; - SolidPaint val = (SolidPaint)(object)((background is SolidPaint) ? background : null); - if (val != null && val.Color != null) - { - ((ViewHandler)(object)handler).PlatformView.BackgroundColor = val.Color.ToSKColor(); - } - } - } + var font = searchBar.Font; + if (font.Size > 0) + handler.PlatformView.FontSize = (float)font.Size; + + if (!string.IsNullOrEmpty(font.Family)) + handler.PlatformView.FontFamily = font.Family; + } + + public static void MapPlaceholder(SearchBarHandler handler, ISearchBar searchBar) + { + if (handler.PlatformView is null) return; + handler.PlatformView.Placeholder = searchBar.Placeholder ?? string.Empty; + } + + public static void MapPlaceholderColor(SearchBarHandler handler, ISearchBar searchBar) + { + if (handler.PlatformView is null) return; + + if (searchBar.PlaceholderColor is not null) + handler.PlatformView.PlaceholderColor = searchBar.PlaceholderColor.ToSKColor(); + } + + public static void MapCancelButtonColor(SearchBarHandler handler, ISearchBar searchBar) + { + if (handler.PlatformView is null) return; + + // CancelButtonColor maps to ClearButtonColor + if (searchBar.CancelButtonColor is not null) + handler.PlatformView.ClearButtonColor = searchBar.CancelButtonColor.ToSKColor(); + } + + + public static void MapBackground(SearchBarHandler handler, ISearchBar searchBar) + { + if (handler.PlatformView is null) return; + + if (searchBar.Background is SolidPaint solidPaint && solidPaint.Color is not null) + { + handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor(); + } + } } diff --git a/Handlers/ShellHandler.cs b/Handlers/ShellHandler.cs index 689c33a..bd7a2b7 100644 --- a/Handlers/ShellHandler.cs +++ b/Handlers/ShellHandler.cs @@ -1,88 +1,93 @@ -using System; -using System.Runtime.CompilerServices; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using Microsoft.Maui.Controls; using Microsoft.Maui.Handlers; +using Microsoft.Maui.Graphics; +using SkiaSharp; namespace Microsoft.Maui.Platform.Linux.Handlers; -public class ShellHandler : ViewHandler +/// +/// Handler for Shell on Linux using Skia rendering. +/// +public partial class ShellHandler : ViewHandler { - public static IPropertyMapper Mapper = (IPropertyMapper)(object)new PropertyMapper((IPropertyMapper[])(object)new IPropertyMapper[1] { (IPropertyMapper)ViewHandler.ViewMapper }); + public static IPropertyMapper Mapper = new PropertyMapper(ViewHandler.ViewMapper) + { + }; - public static CommandMapper CommandMapper = new CommandMapper((CommandMapper)(object)ViewHandler.ViewCommandMapper); + public static CommandMapper CommandMapper = new(ViewHandler.ViewCommandMapper) + { + }; - public ShellHandler() - : base((IPropertyMapper)(object)Mapper, (CommandMapper)(object)CommandMapper) - { - } + public ShellHandler() : base(Mapper, CommandMapper) + { + } - public ShellHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null) - : base((IPropertyMapper)(((object)mapper) ?? ((object)Mapper)), (CommandMapper)(((object)commandMapper) ?? ((object)CommandMapper))) - { - } + public ShellHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null) + : base(mapper ?? Mapper, commandMapper ?? CommandMapper) + { + } - protected override SkiaShell CreatePlatformView() - { - return new SkiaShell(); - } + protected override SkiaShell CreatePlatformView() + { + return new SkiaShell(); + } - protected override void ConnectHandler(SkiaShell platformView) - { - base.ConnectHandler(platformView); - platformView.FlyoutIsPresentedChanged += OnFlyoutIsPresentedChanged; - platformView.Navigated += OnNavigated; - if (base.VirtualView != null) - { - base.VirtualView.Navigating += OnShellNavigating; - base.VirtualView.Navigated += OnShellNavigated; - } - } + protected override void ConnectHandler(SkiaShell platformView) + { + base.ConnectHandler(platformView); + platformView.FlyoutIsPresentedChanged += OnFlyoutIsPresentedChanged; + platformView.Navigated += OnNavigated; - protected override void DisconnectHandler(SkiaShell platformView) - { - platformView.FlyoutIsPresentedChanged -= OnFlyoutIsPresentedChanged; - platformView.Navigated -= OnNavigated; - if (base.VirtualView != null) - { - base.VirtualView.Navigating -= OnShellNavigating; - base.VirtualView.Navigated -= OnShellNavigated; - } - base.DisconnectHandler(platformView); - } + // Subscribe to Shell navigation events + if (VirtualView != null) + { + VirtualView.Navigating += OnShellNavigating; + VirtualView.Navigated += OnShellNavigated; + } + } - private void OnFlyoutIsPresentedChanged(object? sender, EventArgs e) - { - } + protected override void DisconnectHandler(SkiaShell platformView) + { + platformView.FlyoutIsPresentedChanged -= OnFlyoutIsPresentedChanged; + platformView.Navigated -= OnNavigated; - private void OnNavigated(object? sender, ShellNavigationEventArgs e) - { - } + if (VirtualView != null) + { + VirtualView.Navigating -= OnShellNavigating; + VirtualView.Navigated -= OnShellNavigated; + } - private void OnShellNavigating(object? sender, ShellNavigatingEventArgs e) - { - DefaultInterpolatedStringHandler defaultInterpolatedStringHandler = new DefaultInterpolatedStringHandler(36, 1); - defaultInterpolatedStringHandler.AppendLiteral("[ShellHandler] Shell Navigating to: "); - ShellNavigationState target = e.Target; - defaultInterpolatedStringHandler.AppendFormatted((target != null) ? target.Location : null); - Console.WriteLine(defaultInterpolatedStringHandler.ToStringAndClear()); - if (base.PlatformView != null) - { - ShellNavigationState target2 = e.Target; - if (((target2 != null) ? target2.Location : null) != null) - { - string text = e.Target.Location.ToString().TrimStart('/'); - Console.WriteLine("[ShellHandler] Routing to: " + text); - base.PlatformView.GoToAsync(text); - } - } - } + base.DisconnectHandler(platformView); + } - private void OnShellNavigated(object? sender, ShellNavigatedEventArgs e) - { - DefaultInterpolatedStringHandler defaultInterpolatedStringHandler = new DefaultInterpolatedStringHandler(35, 1); - defaultInterpolatedStringHandler.AppendLiteral("[ShellHandler] Shell Navigated to: "); - ShellNavigationState current = e.Current; - defaultInterpolatedStringHandler.AppendFormatted((current != null) ? current.Location : null); - Console.WriteLine(defaultInterpolatedStringHandler.ToStringAndClear()); - } + private void OnFlyoutIsPresentedChanged(object? sender, EventArgs e) + { + // Sync flyout state to virtual view + } + + private void OnNavigated(object? sender, ShellNavigationEventArgs e) + { + // Handle platform navigation events + } + + private void OnShellNavigating(object? sender, ShellNavigatingEventArgs e) + { + Console.WriteLine($"[ShellHandler] Shell Navigating to: {e.Target?.Location}"); + + // Route to platform view + if (PlatformView != null && e.Target?.Location != null) + { + var route = e.Target.Location.ToString().TrimStart('/'); + Console.WriteLine($"[ShellHandler] Routing to: {route}"); + PlatformView.GoToAsync(route); + } + } + + private void OnShellNavigated(object? sender, ShellNavigatedEventArgs e) + { + Console.WriteLine($"[ShellHandler] Shell Navigated to: {e.Current?.Location}"); + } } diff --git a/Handlers/SizeChangedEventArgs.cs b/Handlers/SizeChangedEventArgs.cs deleted file mode 100644 index 830445b..0000000 --- a/Handlers/SizeChangedEventArgs.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; - -namespace Microsoft.Maui.Platform.Linux.Handlers; - -public class SizeChangedEventArgs : EventArgs -{ - public int Width { get; } - - public int Height { get; } - - public SizeChangedEventArgs(int width, int height) - { - Width = width; - Height = height; - } -} diff --git a/Handlers/SkiaWindow.cs b/Handlers/SkiaWindow.cs deleted file mode 100644 index d2ff70a..0000000 --- a/Handlers/SkiaWindow.cs +++ /dev/null @@ -1,184 +0,0 @@ -using System; -using SkiaSharp; - -namespace Microsoft.Maui.Platform.Linux.Handlers; - -public class SkiaWindow -{ - private SkiaView? _content; - - private string _title = "MAUI Application"; - - private int _x; - - private int _y; - - private int _width = 800; - - private int _height = 600; - - private int _minWidth = 100; - - private int _minHeight = 100; - - private int _maxWidth = int.MaxValue; - - private int _maxHeight = int.MaxValue; - - public SkiaView? Content - { - get - { - return _content; - } - set - { - _content = value; - this.ContentChanged?.Invoke(this, EventArgs.Empty); - } - } - - public string Title - { - get - { - return _title; - } - set - { - _title = value; - this.TitleChanged?.Invoke(this, EventArgs.Empty); - } - } - - public int X - { - get - { - return _x; - } - set - { - _x = value; - this.PositionChanged?.Invoke(this, EventArgs.Empty); - } - } - - public int Y - { - get - { - return _y; - } - set - { - _y = value; - this.PositionChanged?.Invoke(this, EventArgs.Empty); - } - } - - public int Width - { - get - { - return _width; - } - set - { - _width = Math.Clamp(value, _minWidth, _maxWidth); - this.SizeChanged?.Invoke(this, new SizeChangedEventArgs(_width, _height)); - } - } - - public int Height - { - get - { - return _height; - } - set - { - _height = Math.Clamp(value, _minHeight, _maxHeight); - this.SizeChanged?.Invoke(this, new SizeChangedEventArgs(_width, _height)); - } - } - - public int MinWidth - { - get - { - return _minWidth; - } - set - { - _minWidth = value; - } - } - - public int MinHeight - { - get - { - return _minHeight; - } - set - { - _minHeight = value; - } - } - - public int MaxWidth - { - get - { - return _maxWidth; - } - set - { - _maxWidth = value; - } - } - - public int MaxHeight - { - get - { - return _maxHeight; - } - set - { - _maxHeight = value; - } - } - - public event EventHandler? ContentChanged; - - public event EventHandler? TitleChanged; - - public event EventHandler? PositionChanged; - - public event EventHandler? SizeChanged; - - public event EventHandler? CloseRequested; - - public void Render(SKCanvas canvas) - { - //IL_0001: Unknown result type (might be due to invalid IL or missing references) - //IL_0027: Unknown result type (might be due to invalid IL or missing references) - //IL_002c: Unknown result type (might be due to invalid IL or missing references) - //IL_0050: Unknown result type (might be due to invalid IL or missing references) - canvas.Clear(SKColors.White); - if (_content != null) - { - _content.Measure(new SKSize((float)_width, (float)_height)); - _content.Arrange(new SKRect(0f, 0f, (float)_width, (float)_height)); - _content.Draw(canvas); - } - SkiaView.DrawPopupOverlays(canvas); - } - - public void Close() - { - this.CloseRequested?.Invoke(this, EventArgs.Empty); - } -} diff --git a/Handlers/SliderHandler.cs b/Handlers/SliderHandler.cs index e35149f..2eb4583 100644 --- a/Handlers/SliderHandler.cs +++ b/Handlers/SliderHandler.cs @@ -1,160 +1,153 @@ -using System; -using Microsoft.Maui.Graphics; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using Microsoft.Maui.Handlers; +using Microsoft.Maui.Graphics; +using SkiaSharp; namespace Microsoft.Maui.Platform.Linux.Handlers; -public class SliderHandler : ViewHandler +/// +/// Handler for Slider on Linux using Skia rendering. +/// Maps ISlider interface to SkiaSlider platform view. +/// +public partial class SliderHandler : ViewHandler { - public static IPropertyMapper Mapper = (IPropertyMapper)(object)new PropertyMapper((IPropertyMapper[])(object)new IPropertyMapper[1] { (IPropertyMapper)ViewHandler.ViewMapper }) - { - ["Minimum"] = MapMinimum, - ["Maximum"] = MapMaximum, - ["Value"] = MapValue, - ["MinimumTrackColor"] = MapMinimumTrackColor, - ["MaximumTrackColor"] = MapMaximumTrackColor, - ["ThumbColor"] = MapThumbColor, - ["Background"] = MapBackground, - ["IsEnabled"] = MapIsEnabled - }; + public static IPropertyMapper Mapper = new PropertyMapper(ViewHandler.ViewMapper) + { + [nameof(IRange.Minimum)] = MapMinimum, + [nameof(IRange.Maximum)] = MapMaximum, + [nameof(IRange.Value)] = MapValue, + [nameof(ISlider.MinimumTrackColor)] = MapMinimumTrackColor, + [nameof(ISlider.MaximumTrackColor)] = MapMaximumTrackColor, + [nameof(ISlider.ThumbColor)] = MapThumbColor, + [nameof(IView.Background)] = MapBackground, + [nameof(IView.IsEnabled)] = MapIsEnabled, + }; - public static CommandMapper CommandMapper = new CommandMapper((CommandMapper)(object)ViewHandler.ViewCommandMapper); + public static CommandMapper CommandMapper = new(ViewHandler.ViewCommandMapper) + { + }; - public SliderHandler() - : base((IPropertyMapper)(object)Mapper, (CommandMapper)(object)CommandMapper) - { - } + public SliderHandler() : base(Mapper, CommandMapper) + { + } - public SliderHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null) - : base((IPropertyMapper)(((object)mapper) ?? ((object)Mapper)), (CommandMapper)(((object)commandMapper) ?? ((object)CommandMapper))) - { - } + public SliderHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null) + : base(mapper ?? Mapper, commandMapper ?? CommandMapper) + { + } - protected override SkiaSlider CreatePlatformView() - { - return new SkiaSlider(); - } + protected override SkiaSlider CreatePlatformView() + { + return new SkiaSlider(); + } - protected override void ConnectHandler(SkiaSlider platformView) - { - base.ConnectHandler(platformView); - platformView.ValueChanged += OnValueChanged; - platformView.DragStarted += OnDragStarted; - platformView.DragCompleted += OnDragCompleted; - if (base.VirtualView != null) - { - MapMinimum(this, base.VirtualView); - MapMaximum(this, base.VirtualView); - MapValue(this, base.VirtualView); - MapIsEnabled(this, base.VirtualView); - } - } + protected override void ConnectHandler(SkiaSlider platformView) + { + base.ConnectHandler(platformView); + platformView.ValueChanged += OnValueChanged; + platformView.DragStarted += OnDragStarted; + platformView.DragCompleted += OnDragCompleted; - protected override void DisconnectHandler(SkiaSlider platformView) - { - platformView.ValueChanged -= OnValueChanged; - platformView.DragStarted -= OnDragStarted; - platformView.DragCompleted -= OnDragCompleted; - base.DisconnectHandler(platformView); - } + // Sync properties that may have been set before handler connection + if (VirtualView != null) + { + MapMinimum(this, VirtualView); + MapMaximum(this, VirtualView); + MapValue(this, VirtualView); + MapIsEnabled(this, VirtualView); + } + } - private void OnValueChanged(object? sender, SliderValueChangedEventArgs e) - { - if (base.VirtualView != null && base.PlatformView != null && Math.Abs(((IRange)base.VirtualView).Value - e.NewValue) > 0.0001) - { - ((IRange)base.VirtualView).Value = e.NewValue; - } - } + protected override void DisconnectHandler(SkiaSlider platformView) + { + platformView.ValueChanged -= OnValueChanged; + platformView.DragStarted -= OnDragStarted; + platformView.DragCompleted -= OnDragCompleted; + base.DisconnectHandler(platformView); + } - private void OnDragStarted(object? sender, EventArgs e) - { - ISlider virtualView = base.VirtualView; - if (virtualView != null) - { - virtualView.DragStarted(); - } - } + private void OnValueChanged(object? sender, SliderValueChangedEventArgs e) + { + if (VirtualView is null || PlatformView is null) return; - private void OnDragCompleted(object? sender, EventArgs e) - { - ISlider virtualView = base.VirtualView; - if (virtualView != null) - { - virtualView.DragCompleted(); - } - } + if (Math.Abs(VirtualView.Value - e.NewValue) > 0.0001) + { + VirtualView.Value = e.NewValue; + } + } - public static void MapMinimum(SliderHandler handler, ISlider slider) - { - if (((ViewHandler)(object)handler).PlatformView != null) - { - ((ViewHandler)(object)handler).PlatformView.Minimum = ((IRange)slider).Minimum; - } - } + private void OnDragStarted(object? sender, EventArgs e) + { + VirtualView?.DragStarted(); + } - public static void MapMaximum(SliderHandler handler, ISlider slider) - { - if (((ViewHandler)(object)handler).PlatformView != null) - { - ((ViewHandler)(object)handler).PlatformView.Maximum = ((IRange)slider).Maximum; - } - } + private void OnDragCompleted(object? sender, EventArgs e) + { + VirtualView?.DragCompleted(); + } - public static void MapValue(SliderHandler handler, ISlider slider) - { - if (((ViewHandler)(object)handler).PlatformView != null && Math.Abs(((ViewHandler)(object)handler).PlatformView.Value - ((IRange)slider).Value) > 0.0001) - { - ((ViewHandler)(object)handler).PlatformView.Value = ((IRange)slider).Value; - } - } + public static void MapMinimum(SliderHandler handler, ISlider slider) + { + if (handler.PlatformView is null) return; + handler.PlatformView.Minimum = slider.Minimum; + } - public static void MapMinimumTrackColor(SliderHandler handler, ISlider slider) - { - //IL_001d: Unknown result type (might be due to invalid IL or missing references) - if (((ViewHandler)(object)handler).PlatformView != null && slider.MinimumTrackColor != null) - { - ((ViewHandler)(object)handler).PlatformView.ActiveTrackColor = slider.MinimumTrackColor.ToSKColor(); - } - } + public static void MapMaximum(SliderHandler handler, ISlider slider) + { + if (handler.PlatformView is null) return; + handler.PlatformView.Maximum = slider.Maximum; + } - public static void MapMaximumTrackColor(SliderHandler handler, ISlider slider) - { - //IL_001d: Unknown result type (might be due to invalid IL or missing references) - if (((ViewHandler)(object)handler).PlatformView != null && slider.MaximumTrackColor != null) - { - ((ViewHandler)(object)handler).PlatformView.TrackColor = slider.MaximumTrackColor.ToSKColor(); - } - } + public static void MapValue(SliderHandler handler, ISlider slider) + { + if (handler.PlatformView is null) return; - public static void MapThumbColor(SliderHandler handler, ISlider slider) - { - //IL_001d: Unknown result type (might be due to invalid IL or missing references) - if (((ViewHandler)(object)handler).PlatformView != null && slider.ThumbColor != null) - { - ((ViewHandler)(object)handler).PlatformView.ThumbColor = slider.ThumbColor.ToSKColor(); - } - } + if (Math.Abs(handler.PlatformView.Value - slider.Value) > 0.0001) + handler.PlatformView.Value = slider.Value; + } - public static void MapBackground(SliderHandler handler, ISlider slider) - { - //IL_002c: Unknown result type (might be due to invalid IL or missing references) - if (((ViewHandler)(object)handler).PlatformView != null) - { - Paint background = ((IView)slider).Background; - SolidPaint val = (SolidPaint)(object)((background is SolidPaint) ? background : null); - if (val != null && val.Color != null) - { - ((ViewHandler)(object)handler).PlatformView.BackgroundColor = val.Color.ToSKColor(); - } - } - } + public static void MapMinimumTrackColor(SliderHandler handler, ISlider slider) + { + if (handler.PlatformView is null) return; - public static void MapIsEnabled(SliderHandler handler, ISlider slider) - { - if (((ViewHandler)(object)handler).PlatformView != null) - { - ((ViewHandler)(object)handler).PlatformView.IsEnabled = ((IView)slider).IsEnabled; - ((ViewHandler)(object)handler).PlatformView.Invalidate(); - } - } + // MinimumTrackColor maps to ActiveTrackColor (the filled portion) + if (slider.MinimumTrackColor is not null) + handler.PlatformView.ActiveTrackColor = slider.MinimumTrackColor.ToSKColor(); + } + + public static void MapMaximumTrackColor(SliderHandler handler, ISlider slider) + { + if (handler.PlatformView is null) return; + + // MaximumTrackColor maps to TrackColor (the unfilled portion) + if (slider.MaximumTrackColor is not null) + handler.PlatformView.TrackColor = slider.MaximumTrackColor.ToSKColor(); + } + + public static void MapThumbColor(SliderHandler handler, ISlider slider) + { + if (handler.PlatformView is null) return; + + if (slider.ThumbColor is not null) + handler.PlatformView.ThumbColor = slider.ThumbColor.ToSKColor(); + } + + public static void MapBackground(SliderHandler handler, ISlider slider) + { + if (handler.PlatformView is null) return; + + if (slider.Background is SolidPaint solidPaint && solidPaint.Color is not null) + { + handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor(); + } + } + + public static void MapIsEnabled(SliderHandler handler, ISlider slider) + { + if (handler.PlatformView is null) return; + handler.PlatformView.IsEnabled = slider.IsEnabled; + handler.PlatformView.Invalidate(); + } } diff --git a/Handlers/StackLayoutHandler.cs b/Handlers/StackLayoutHandler.cs deleted file mode 100644 index 5fb4b71..0000000 --- a/Handlers/StackLayoutHandler.cs +++ /dev/null @@ -1,49 +0,0 @@ -using Microsoft.Maui.Controls; -using Microsoft.Maui.Handlers; - -namespace Microsoft.Maui.Platform.Linux.Handlers; - -public class StackLayoutHandler : LayoutHandler -{ - public new static IPropertyMapper Mapper = (IPropertyMapper)(object)new PropertyMapper((IPropertyMapper[])(object)new IPropertyMapper[1] { (IPropertyMapper)LayoutHandler.Mapper }) { ["Spacing"] = MapSpacing }; - - public StackLayoutHandler() - : base((IPropertyMapper?)(object)Mapper) - { - } - - protected override SkiaLayoutView CreatePlatformView() - { - return new SkiaStackLayout(); - } - - protected override void ConnectHandler(SkiaLayoutView platformView) - { - if (platformView is SkiaStackLayout skiaStackLayout) - { - ILayout virtualView = ((ViewHandler)(object)this).VirtualView; - IStackLayout val = (IStackLayout)(object)((virtualView is IStackLayout) ? virtualView : null); - if (val != null) - { - if (((ViewHandler)(object)this).VirtualView is HorizontalStackLayout) - { - skiaStackLayout.Orientation = StackOrientation.Horizontal; - } - else if (((ViewHandler)(object)this).VirtualView is VerticalStackLayout || ((ViewHandler)(object)this).VirtualView is StackLayout) - { - skiaStackLayout.Orientation = StackOrientation.Vertical; - } - skiaStackLayout.Spacing = (float)val.Spacing; - } - } - base.ConnectHandler(platformView); - } - - public static void MapSpacing(StackLayoutHandler handler, IStackLayout layout) - { - if (((ViewHandler)(object)handler).PlatformView is SkiaStackLayout skiaStackLayout) - { - skiaStackLayout.Spacing = (float)layout.Spacing; - } - } -} diff --git a/Handlers/StepperHandler.cs b/Handlers/StepperHandler.cs index 8c89c66..4652a1e 100644 --- a/Handlers/StepperHandler.cs +++ b/Handlers/StepperHandler.cs @@ -1,133 +1,89 @@ -using System; -using Microsoft.Maui.Controls; -using Microsoft.Maui.Graphics; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using Microsoft.Maui.Handlers; +using Microsoft.Maui.Graphics; +using Microsoft.Maui.Platform; using SkiaSharp; namespace Microsoft.Maui.Platform.Linux.Handlers; -public class StepperHandler : ViewHandler +/// +/// Handler for Stepper on Linux using Skia rendering. +/// +public partial class StepperHandler : ViewHandler { - public static IPropertyMapper Mapper = (IPropertyMapper)(object)new PropertyMapper((IPropertyMapper[])(object)new IPropertyMapper[1] { (IPropertyMapper)ViewHandler.ViewMapper }) - { - ["Value"] = MapValue, - ["Minimum"] = MapMinimum, - ["Maximum"] = MapMaximum, - ["Increment"] = MapIncrement, - ["Background"] = MapBackground, - ["IsEnabled"] = MapIsEnabled - }; + public static IPropertyMapper Mapper = + new PropertyMapper(ViewHandler.ViewMapper) + { + [nameof(IStepper.Value)] = MapValue, + [nameof(IStepper.Minimum)] = MapMinimum, + [nameof(IStepper.Maximum)] = MapMaximum, + [nameof(IView.Background)] = MapBackground, + }; - public static CommandMapper CommandMapper = new CommandMapper((CommandMapper)(object)ViewHandler.ViewCommandMapper); + public static CommandMapper CommandMapper = + new(ViewHandler.ViewCommandMapper) + { + }; - public StepperHandler() - : base((IPropertyMapper)(object)Mapper, (CommandMapper)(object)CommandMapper) - { - } + public StepperHandler() : base(Mapper, CommandMapper) + { + } - public StepperHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null) - : base((IPropertyMapper)(((object)mapper) ?? ((object)Mapper)), (CommandMapper)(((object)commandMapper) ?? ((object)CommandMapper))) - { - } + public StepperHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null) + : base(mapper ?? Mapper, commandMapper ?? CommandMapper) + { + } - protected override SkiaStepper CreatePlatformView() - { - return new SkiaStepper(); - } + protected override SkiaStepper CreatePlatformView() + { + return new SkiaStepper(); + } - protected override void ConnectHandler(SkiaStepper platformView) - { - //IL_0025: Unknown result type (might be due to invalid IL or missing references) - //IL_002b: Invalid comparison between Unknown and I4 - //IL_0036: Unknown result type (might be due to invalid IL or missing references) - //IL_0047: Unknown result type (might be due to invalid IL or missing references) - //IL_0058: Unknown result type (might be due to invalid IL or missing references) - //IL_0072: Unknown result type (might be due to invalid IL or missing references) - //IL_0083: Unknown result type (might be due to invalid IL or missing references) - //IL_0094: Unknown result type (might be due to invalid IL or missing references) - base.ConnectHandler(platformView); - platformView.ValueChanged += OnValueChanged; - Application current = Application.Current; - if (current != null && (int)current.UserAppTheme == 2) - { - platformView.ButtonBackgroundColor = new SKColor((byte)66, (byte)66, (byte)66); - platformView.ButtonPressedColor = new SKColor((byte)97, (byte)97, (byte)97); - platformView.ButtonDisabledColor = new SKColor((byte)48, (byte)48, (byte)48); - platformView.SymbolColor = new SKColor((byte)224, (byte)224, (byte)224); - platformView.SymbolDisabledColor = new SKColor((byte)97, (byte)97, (byte)97); - platformView.BorderColor = new SKColor((byte)97, (byte)97, (byte)97); - } - } + protected override void ConnectHandler(SkiaStepper platformView) + { + base.ConnectHandler(platformView); + platformView.ValueChanged += OnValueChanged; + } - protected override void DisconnectHandler(SkiaStepper platformView) - { - platformView.ValueChanged -= OnValueChanged; - base.DisconnectHandler(platformView); - } + protected override void DisconnectHandler(SkiaStepper platformView) + { + platformView.ValueChanged -= OnValueChanged; + base.DisconnectHandler(platformView); + } - private void OnValueChanged(object? sender, EventArgs e) - { - if (base.VirtualView != null && base.PlatformView != null) - { - ((IRange)base.VirtualView).Value = base.PlatformView.Value; - } - } + private void OnValueChanged(object? sender, EventArgs e) + { + if (VirtualView is null || PlatformView is null) return; + VirtualView.Value = PlatformView.Value; + } - public static void MapValue(StepperHandler handler, IStepper stepper) - { - if (((ViewHandler)(object)handler).PlatformView != null) - { - ((ViewHandler)(object)handler).PlatformView.Value = ((IRange)stepper).Value; - } - } + public static void MapValue(StepperHandler handler, IStepper stepper) + { + if (handler.PlatformView is null) return; + handler.PlatformView.Value = stepper.Value; + } - public static void MapMinimum(StepperHandler handler, IStepper stepper) - { - if (((ViewHandler)(object)handler).PlatformView != null) - { - ((ViewHandler)(object)handler).PlatformView.Minimum = ((IRange)stepper).Minimum; - } - } + public static void MapMinimum(StepperHandler handler, IStepper stepper) + { + if (handler.PlatformView is null) return; + handler.PlatformView.Minimum = stepper.Minimum; + } - public static void MapMaximum(StepperHandler handler, IStepper stepper) - { - if (((ViewHandler)(object)handler).PlatformView != null) - { - ((ViewHandler)(object)handler).PlatformView.Maximum = ((IRange)stepper).Maximum; - } - } + public static void MapMaximum(StepperHandler handler, IStepper stepper) + { + if (handler.PlatformView is null) return; + handler.PlatformView.Maximum = stepper.Maximum; + } - public static void MapBackground(StepperHandler handler, IStepper stepper) - { - //IL_002c: Unknown result type (might be due to invalid IL or missing references) - if (((ViewHandler)(object)handler).PlatformView != null) - { - Paint background = ((IView)stepper).Background; - SolidPaint val = (SolidPaint)(object)((background is SolidPaint) ? background : null); - if (val != null && val.Color != null) - { - ((ViewHandler)(object)handler).PlatformView.BackgroundColor = val.Color.ToSKColor(); - } - } - } + public static void MapBackground(StepperHandler handler, IStepper stepper) + { + if (handler.PlatformView is null) return; - public static void MapIncrement(StepperHandler handler, IStepper stepper) - { - if (((ViewHandler)(object)handler).PlatformView != null) - { - Stepper val = (Stepper)(object)((stepper is Stepper) ? stepper : null); - if (val != null) - { - ((ViewHandler)(object)handler).PlatformView.Increment = val.Increment; - } - } - } - - public static void MapIsEnabled(StepperHandler handler, IStepper stepper) - { - if (((ViewHandler)(object)handler).PlatformView != null) - { - ((ViewHandler)(object)handler).PlatformView.IsEnabled = ((IView)stepper).IsEnabled; - } - } + if (stepper.Background is SolidPaint solidPaint && solidPaint.Color is not null) + { + handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor(); + } + } } diff --git a/Handlers/SwitchHandler.cs b/Handlers/SwitchHandler.cs index e05c85d..86a4b28 100644 --- a/Handlers/SwitchHandler.cs +++ b/Handlers/SwitchHandler.cs @@ -1,107 +1,99 @@ -using Microsoft.Maui.Graphics; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using Microsoft.Maui.Handlers; +using Microsoft.Maui.Graphics; using SkiaSharp; namespace Microsoft.Maui.Platform.Linux.Handlers; -public class SwitchHandler : ViewHandler +/// +/// Handler for Switch on Linux using Skia rendering. +/// Maps ISwitch interface to SkiaSwitch platform view. +/// +public partial class SwitchHandler : ViewHandler { - public static IPropertyMapper Mapper = (IPropertyMapper)(object)new PropertyMapper((IPropertyMapper[])(object)new IPropertyMapper[1] { (IPropertyMapper)ViewHandler.ViewMapper }) - { - ["IsOn"] = MapIsOn, - ["TrackColor"] = MapTrackColor, - ["ThumbColor"] = MapThumbColor, - ["Background"] = MapBackground, - ["IsEnabled"] = MapIsEnabled - }; + public static IPropertyMapper Mapper = new PropertyMapper(ViewHandler.ViewMapper) + { + [nameof(ISwitch.IsOn)] = MapIsOn, + [nameof(ISwitch.TrackColor)] = MapTrackColor, + [nameof(ISwitch.ThumbColor)] = MapThumbColor, + [nameof(IView.Background)] = MapBackground, + }; - public static CommandMapper CommandMapper = new CommandMapper((CommandMapper)(object)ViewHandler.ViewCommandMapper); + public static CommandMapper CommandMapper = new(ViewHandler.ViewCommandMapper) + { + }; - public SwitchHandler() - : base((IPropertyMapper)(object)Mapper, (CommandMapper)(object)CommandMapper) - { - } + public SwitchHandler() : base(Mapper, CommandMapper) + { + } - public SwitchHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null) - : base((IPropertyMapper)(((object)mapper) ?? ((object)Mapper)), (CommandMapper)(((object)commandMapper) ?? ((object)CommandMapper))) - { - } + public SwitchHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null) + : base(mapper ?? Mapper, commandMapper ?? CommandMapper) + { + } - protected override SkiaSwitch CreatePlatformView() - { - return new SkiaSwitch(); - } + protected override SkiaSwitch CreatePlatformView() + { + return new SkiaSwitch(); + } - protected override void ConnectHandler(SkiaSwitch platformView) - { - base.ConnectHandler(platformView); - platformView.Toggled += OnToggled; - } + protected override void ConnectHandler(SkiaSwitch platformView) + { + base.ConnectHandler(platformView); + platformView.Toggled += OnToggled; + } - protected override void DisconnectHandler(SkiaSwitch platformView) - { - platformView.Toggled -= OnToggled; - base.DisconnectHandler(platformView); - } + protected override void DisconnectHandler(SkiaSwitch platformView) + { + platformView.Toggled -= OnToggled; + base.DisconnectHandler(platformView); + } - private void OnToggled(object? sender, ToggledEventArgs e) - { - if (base.VirtualView != null && base.VirtualView.IsOn != e.Value) - { - base.VirtualView.IsOn = e.Value; - } - } + private void OnToggled(object? sender, Platform.ToggledEventArgs e) + { + if (VirtualView is not null && VirtualView.IsOn != e.Value) + { + VirtualView.IsOn = e.Value; + } + } - public static void MapIsOn(SwitchHandler handler, ISwitch @switch) - { - if (((ViewHandler)(object)handler).PlatformView != null) - { - ((ViewHandler)(object)handler).PlatformView.IsOn = @switch.IsOn; - } - } + public static void MapIsOn(SwitchHandler handler, ISwitch @switch) + { + if (handler.PlatformView is null) return; + handler.PlatformView.IsOn = @switch.IsOn; + } - public static void MapTrackColor(SwitchHandler handler, ISwitch @switch) - { - //IL_0017: Unknown result type (might be due to invalid IL or missing references) - //IL_001c: Unknown result type (might be due to invalid IL or missing references) - //IL_0023: Unknown result type (might be due to invalid IL or missing references) - //IL_0036: Unknown result type (might be due to invalid IL or missing references) - if (((ViewHandler)(object)handler).PlatformView != null && @switch.TrackColor != null) - { - SKColor onTrackColor = @switch.TrackColor.ToSKColor(); - ((ViewHandler)(object)handler).PlatformView.OnTrackColor = onTrackColor; - ((ViewHandler)(object)handler).PlatformView.OffTrackColor = ((SKColor)(ref onTrackColor)).WithAlpha((byte)128); - } - } + public static void MapTrackColor(SwitchHandler handler, ISwitch @switch) + { + if (handler.PlatformView is null) return; - public static void MapThumbColor(SwitchHandler handler, ISwitch @switch) - { - //IL_001d: Unknown result type (might be due to invalid IL or missing references) - if (((ViewHandler)(object)handler).PlatformView != null && @switch.ThumbColor != null) - { - ((ViewHandler)(object)handler).PlatformView.ThumbColor = @switch.ThumbColor.ToSKColor(); - } - } + // TrackColor sets both On and Off track colors + if (@switch.TrackColor is not null) + { + var color = @switch.TrackColor.ToSKColor(); + handler.PlatformView.OnTrackColor = color; + // Off track could be a lighter version + handler.PlatformView.OffTrackColor = color.WithAlpha(128); + } + } - public static void MapBackground(SwitchHandler handler, ISwitch @switch) - { - //IL_002c: Unknown result type (might be due to invalid IL or missing references) - if (((ViewHandler)(object)handler).PlatformView != null) - { - Paint background = ((IView)@switch).Background; - SolidPaint val = (SolidPaint)(object)((background is SolidPaint) ? background : null); - if (val != null && val.Color != null) - { - ((ViewHandler)(object)handler).PlatformView.BackgroundColor = val.Color.ToSKColor(); - } - } - } + public static void MapThumbColor(SwitchHandler handler, ISwitch @switch) + { + if (handler.PlatformView is null) return; - public static void MapIsEnabled(SwitchHandler handler, ISwitch @switch) - { - if (((ViewHandler)(object)handler).PlatformView != null) - { - ((ViewHandler)(object)handler).PlatformView.IsEnabled = ((IView)@switch).IsEnabled; - } - } + if (@switch.ThumbColor is not null) + handler.PlatformView.ThumbColor = @switch.ThumbColor.ToSKColor(); + } + + public static void MapBackground(SwitchHandler handler, ISwitch @switch) + { + if (handler.PlatformView is null) return; + + if (@switch.Background is SolidPaint solidPaint && solidPaint.Color is not null) + { + handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor(); + } + } } diff --git a/Handlers/TabbedPageHandler.cs b/Handlers/TabbedPageHandler.cs index 23cd1a1..b306004 100644 --- a/Handlers/TabbedPageHandler.cs +++ b/Handlers/TabbedPageHandler.cs @@ -1,43 +1,55 @@ -using System; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using Microsoft.Maui.Handlers; +using Microsoft.Maui.Graphics; +using SkiaSharp; namespace Microsoft.Maui.Platform.Linux.Handlers; -public class TabbedPageHandler : ViewHandler +/// +/// Handler for TabbedPage on Linux using Skia rendering. +/// Maps ITabbedView interface to SkiaTabbedPage platform view. +/// +public partial class TabbedPageHandler : ViewHandler { - public static IPropertyMapper Mapper = (IPropertyMapper)(object)new PropertyMapper((IPropertyMapper[])(object)new IPropertyMapper[1] { (IPropertyMapper)ViewHandler.ViewMapper }); + public static IPropertyMapper Mapper = new PropertyMapper(ViewHandler.ViewMapper) + { + }; - public static CommandMapper CommandMapper = new CommandMapper((CommandMapper)(object)ViewHandler.ViewCommandMapper); + public static CommandMapper CommandMapper = new(ViewHandler.ViewCommandMapper) + { + }; - public TabbedPageHandler() - : base((IPropertyMapper)(object)Mapper, (CommandMapper)(object)CommandMapper) - { - } + public TabbedPageHandler() : base(Mapper, CommandMapper) + { + } - public TabbedPageHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null) - : base((IPropertyMapper)(((object)mapper) ?? ((object)Mapper)), (CommandMapper)(((object)commandMapper) ?? ((object)CommandMapper))) - { - } + public TabbedPageHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null) + : base(mapper ?? Mapper, commandMapper ?? CommandMapper) + { + } - protected override SkiaTabbedPage CreatePlatformView() - { - return new SkiaTabbedPage(); - } + protected override SkiaTabbedPage CreatePlatformView() + { + return new SkiaTabbedPage(); + } - protected override void ConnectHandler(SkiaTabbedPage platformView) - { - base.ConnectHandler(platformView); - platformView.SelectedIndexChanged += OnSelectedIndexChanged; - } + protected override void ConnectHandler(SkiaTabbedPage platformView) + { + base.ConnectHandler(platformView); + platformView.SelectedIndexChanged += OnSelectedIndexChanged; + } - protected override void DisconnectHandler(SkiaTabbedPage platformView) - { - platformView.SelectedIndexChanged -= OnSelectedIndexChanged; - platformView.ClearTabs(); - base.DisconnectHandler(platformView); - } + protected override void DisconnectHandler(SkiaTabbedPage platformView) + { + platformView.SelectedIndexChanged -= OnSelectedIndexChanged; + platformView.ClearTabs(); + base.DisconnectHandler(platformView); + } - private void OnSelectedIndexChanged(object? sender, EventArgs e) - { - } + private void OnSelectedIndexChanged(object? sender, EventArgs e) + { + // Notify the virtual view of selection change + } } diff --git a/Handlers/TextButtonHandler.cs b/Handlers/TextButtonHandler.cs deleted file mode 100644 index 669809f..0000000 --- a/Handlers/TextButtonHandler.cs +++ /dev/null @@ -1,84 +0,0 @@ -using Microsoft.Maui.Handlers; - -namespace Microsoft.Maui.Platform.Linux.Handlers; - -public class TextButtonHandler : ButtonHandler -{ - public new static IPropertyMapper Mapper = (IPropertyMapper)(object)new PropertyMapper((IPropertyMapper[])(object)new IPropertyMapper[1] { (IPropertyMapper)ButtonHandler.Mapper }) - { - ["Text"] = MapText, - ["TextColor"] = MapTextColor, - ["Font"] = MapFont, - ["CharacterSpacing"] = MapCharacterSpacing - }; - - public TextButtonHandler() - : base((IPropertyMapper?)(object)Mapper) - { - } - - protected override void ConnectHandler(SkiaButton platformView) - { - base.ConnectHandler(platformView); - IButton virtualView = ((ViewHandler)(object)this).VirtualView; - ITextButton val = (ITextButton)(object)((virtualView is ITextButton) ? virtualView : null); - if (val != null) - { - MapText(this, val); - MapTextColor(this, val); - MapFont(this, val); - MapCharacterSpacing(this, val); - } - } - - public static void MapText(TextButtonHandler handler, ITextButton button) - { - if (((ViewHandler)(object)handler).PlatformView != null) - { - ((ViewHandler)(object)handler).PlatformView.Text = ((IText)button).Text ?? string.Empty; - } - } - - public static void MapTextColor(TextButtonHandler handler, ITextButton button) - { - //IL_001d: Unknown result type (might be due to invalid IL or missing references) - if (((ViewHandler)(object)handler).PlatformView != null && ((ITextStyle)button).TextColor != null) - { - ((ViewHandler)(object)handler).PlatformView.TextColor = ((ITextStyle)button).TextColor.ToSKColor(); - } - } - - public static void MapFont(TextButtonHandler handler, ITextButton button) - { - //IL_000a: Unknown result type (might be due to invalid IL or missing references) - //IL_000f: Unknown result type (might be due to invalid IL or missing references) - //IL_005d: Unknown result type (might be due to invalid IL or missing references) - //IL_0067: Invalid comparison between Unknown and I4 - //IL_0079: Unknown result type (might be due to invalid IL or missing references) - //IL_007f: Invalid comparison between Unknown and I4 - //IL_0083: Unknown result type (might be due to invalid IL or missing references) - //IL_0089: Invalid comparison between Unknown and I4 - if (((ViewHandler)(object)handler).PlatformView != null) - { - Font font = ((ITextStyle)button).Font; - if (((Font)(ref font)).Size > 0.0) - { - ((ViewHandler)(object)handler).PlatformView.FontSize = (float)((Font)(ref font)).Size; - } - if (!string.IsNullOrEmpty(((Font)(ref font)).Family)) - { - ((ViewHandler)(object)handler).PlatformView.FontFamily = ((Font)(ref font)).Family; - } - ((ViewHandler)(object)handler).PlatformView.IsBold = (int)((Font)(ref font)).Weight >= 700; - ((ViewHandler)(object)handler).PlatformView.IsItalic = (int)((Font)(ref font)).Slant == 1 || (int)((Font)(ref font)).Slant == 2; - } - } - - public static void MapCharacterSpacing(TextButtonHandler handler, ITextButton button) - { - if (((ViewHandler)(object)handler).PlatformView != null) - { - ((ViewHandler)(object)handler).PlatformView.CharacterSpacing = (float)((ITextStyle)button).CharacterSpacing; - } - } -} diff --git a/Handlers/TimePickerHandler.cs b/Handlers/TimePickerHandler.cs index d0478bc..6bac774 100644 --- a/Handlers/TimePickerHandler.cs +++ b/Handlers/TimePickerHandler.cs @@ -1,115 +1,100 @@ -using System; -using Microsoft.Maui.Controls; -using Microsoft.Maui.Graphics; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using Microsoft.Maui.Handlers; +using Microsoft.Maui.Graphics; +using Microsoft.Maui.Controls; +using Microsoft.Maui.Platform; using SkiaSharp; namespace Microsoft.Maui.Platform.Linux.Handlers; -public class TimePickerHandler : ViewHandler +/// +/// Handler for TimePicker on Linux using Skia rendering. +/// +public partial class TimePickerHandler : ViewHandler { - public static IPropertyMapper Mapper = (IPropertyMapper)(object)new PropertyMapper((IPropertyMapper[])(object)new IPropertyMapper[1] { (IPropertyMapper)ViewHandler.ViewMapper }) - { - ["Time"] = MapTime, - ["Format"] = MapFormat, - ["TextColor"] = MapTextColor, - ["CharacterSpacing"] = MapCharacterSpacing, - ["Background"] = MapBackground - }; + public static IPropertyMapper Mapper = + new PropertyMapper(ViewHandler.ViewMapper) + { + [nameof(ITimePicker.Time)] = MapTime, + [nameof(ITimePicker.Format)] = MapFormat, + [nameof(ITimePicker.TextColor)] = MapTextColor, + [nameof(ITimePicker.CharacterSpacing)] = MapCharacterSpacing, + [nameof(IView.Background)] = MapBackground, + }; - public static CommandMapper CommandMapper = new CommandMapper((CommandMapper)(object)ViewHandler.ViewCommandMapper); + public static CommandMapper CommandMapper = + new(ViewHandler.ViewCommandMapper) + { + }; - public TimePickerHandler() - : base((IPropertyMapper)(object)Mapper, (CommandMapper)(object)CommandMapper) - { - } + public TimePickerHandler() : base(Mapper, CommandMapper) + { + } - public TimePickerHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null) - : base((IPropertyMapper)(((object)mapper) ?? ((object)Mapper)), (CommandMapper)(((object)commandMapper) ?? ((object)CommandMapper))) - { - } + public TimePickerHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null) + : base(mapper ?? Mapper, commandMapper ?? CommandMapper) + { + } - protected override SkiaTimePicker CreatePlatformView() - { - return new SkiaTimePicker(); - } + protected override SkiaTimePicker CreatePlatformView() + { + return new SkiaTimePicker(); + } - protected override void ConnectHandler(SkiaTimePicker platformView) - { - //IL_0025: Unknown result type (might be due to invalid IL or missing references) - //IL_002b: Invalid comparison between Unknown and I4 - //IL_0036: Unknown result type (might be due to invalid IL or missing references) - //IL_0047: Unknown result type (might be due to invalid IL or missing references) - //IL_0061: Unknown result type (might be due to invalid IL or missing references) - //IL_0072: Unknown result type (might be due to invalid IL or missing references) - //IL_0083: Unknown result type (might be due to invalid IL or missing references) - base.ConnectHandler(platformView); - platformView.TimeSelected += OnTimeSelected; - Application current = Application.Current; - if (current != null && (int)current.UserAppTheme == 2) - { - platformView.ClockBackgroundColor = new SKColor((byte)30, (byte)30, (byte)30); - platformView.ClockFaceColor = new SKColor((byte)45, (byte)45, (byte)45); - platformView.TextColor = new SKColor((byte)224, (byte)224, (byte)224); - platformView.BorderColor = new SKColor((byte)97, (byte)97, (byte)97); - platformView.BackgroundColor = new SKColor((byte)45, (byte)45, (byte)45); - } - } + protected override void ConnectHandler(SkiaTimePicker platformView) + { + base.ConnectHandler(platformView); + platformView.TimeSelected += OnTimeSelected; + } - protected override void DisconnectHandler(SkiaTimePicker platformView) - { - platformView.TimeSelected -= OnTimeSelected; - base.DisconnectHandler(platformView); - } + protected override void DisconnectHandler(SkiaTimePicker platformView) + { + platformView.TimeSelected -= OnTimeSelected; + base.DisconnectHandler(platformView); + } - private void OnTimeSelected(object? sender, EventArgs e) - { - if (base.VirtualView != null && base.PlatformView != null) - { - base.VirtualView.Time = base.PlatformView.Time; - } - } + private void OnTimeSelected(object? sender, EventArgs e) + { + if (VirtualView is null || PlatformView is null) return; - public static void MapTime(TimePickerHandler handler, ITimePicker timePicker) - { - if (((ViewHandler)(object)handler).PlatformView != null) - { - ((ViewHandler)(object)handler).PlatformView.Time = timePicker.Time; - } - } + VirtualView.Time = PlatformView.Time; + } - public static void MapFormat(TimePickerHandler handler, ITimePicker timePicker) - { - if (((ViewHandler)(object)handler).PlatformView != null) - { - ((ViewHandler)(object)handler).PlatformView.Format = timePicker.Format ?? "t"; - } - } + public static void MapTime(TimePickerHandler handler, ITimePicker timePicker) + { + if (handler.PlatformView is null) return; + handler.PlatformView.Time = timePicker.Time; + } - public static void MapTextColor(TimePickerHandler handler, ITimePicker timePicker) - { - //IL_001d: Unknown result type (might be due to invalid IL or missing references) - if (((ViewHandler)(object)handler).PlatformView != null && ((ITextStyle)timePicker).TextColor != null) - { - ((ViewHandler)(object)handler).PlatformView.TextColor = ((ITextStyle)timePicker).TextColor.ToSKColor(); - } - } + public static void MapFormat(TimePickerHandler handler, ITimePicker timePicker) + { + if (handler.PlatformView is null) return; + handler.PlatformView.Format = timePicker.Format ?? "t"; + } - public static void MapCharacterSpacing(TimePickerHandler handler, ITimePicker timePicker) - { - } + public static void MapTextColor(TimePickerHandler handler, ITimePicker timePicker) + { + if (handler.PlatformView is null) return; + if (timePicker.TextColor is not null) + { + handler.PlatformView.TextColor = timePicker.TextColor.ToSKColor(); + } + } - public static void MapBackground(TimePickerHandler handler, ITimePicker timePicker) - { - //IL_002c: Unknown result type (might be due to invalid IL or missing references) - if (((ViewHandler)(object)handler).PlatformView != null) - { - Paint background = ((IView)timePicker).Background; - SolidPaint val = (SolidPaint)(object)((background is SolidPaint) ? background : null); - if (val != null && val.Color != null) - { - ((ViewHandler)(object)handler).PlatformView.BackgroundColor = val.Color.ToSKColor(); - } - } - } + public static void MapCharacterSpacing(TimePickerHandler handler, ITimePicker timePicker) + { + // Character spacing would require custom text rendering + } + + public static void MapBackground(TimePickerHandler handler, ITimePicker timePicker) + { + if (handler.PlatformView is null) return; + + if (timePicker.Background is SolidPaint solidPaint && solidPaint.Color is not null) + { + handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor(); + } + } } diff --git a/Handlers/WebViewHandler.Linux.cs b/Handlers/WebViewHandler.Linux.cs deleted file mode 100644 index 49ffdf0..0000000 --- a/Handlers/WebViewHandler.Linux.cs +++ /dev/null @@ -1,207 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.Maui.Handlers; - -namespace Microsoft.Maui.Platform; - -/// -/// Linux handler for WebView control using WebKitGTK. -/// -public partial class WebViewHandler : ViewHandler -{ - /// - /// Property mapper for WebView properties. - /// - public static IPropertyMapper Mapper = new PropertyMapper(ViewHandler.ViewMapper) - { - [nameof(IWebView.Source)] = MapSource, - [nameof(IWebView.UserAgent)] = MapUserAgent, - }; - - /// - /// Command mapper for WebView commands. - /// - public static CommandMapper CommandMapper = new(ViewHandler.ViewCommandMapper) - { - [nameof(IWebView.GoBack)] = MapGoBack, - [nameof(IWebView.GoForward)] = MapGoForward, - [nameof(IWebView.Reload)] = MapReload, - [nameof(IWebView.Eval)] = MapEval, - [nameof(IWebView.EvaluateJavaScriptAsync)] = MapEvaluateJavaScriptAsync, - }; - - public WebViewHandler() : base(Mapper, CommandMapper) - { - } - - public WebViewHandler(IPropertyMapper? mapper) - : base(mapper ?? Mapper, CommandMapper) - { - } - - public WebViewHandler(IPropertyMapper? mapper, CommandMapper? commandMapper) - : base(mapper ?? Mapper, commandMapper ?? CommandMapper) - { - } - - protected override LinuxWebView CreatePlatformView() - { - Console.WriteLine("[WebViewHandler] Creating LinuxWebView"); - return new LinuxWebView(); - } - - protected override void ConnectHandler(LinuxWebView platformView) - { - base.ConnectHandler(platformView); - - platformView.Navigating += OnNavigating; - platformView.Navigated += OnNavigated; - - // Map initial properties - if (VirtualView != null) - { - MapSource(this, VirtualView); - MapUserAgent(this, VirtualView); - } - - Console.WriteLine("[WebViewHandler] Handler connected"); - } - - protected override void DisconnectHandler(LinuxWebView platformView) - { - platformView.Navigating -= OnNavigating; - platformView.Navigated -= OnNavigated; - - base.DisconnectHandler(platformView); - Console.WriteLine("[WebViewHandler] Handler disconnected"); - } - - private void OnNavigating(object? sender, WebViewNavigatingEventArgs e) - { - if (VirtualView == null) - return; - - // Notify the virtual view about navigation starting - VirtualView.Navigating(WebNavigationEvent.NewPage, e.Url); - } - - private void OnNavigated(object? sender, WebViewNavigatedEventArgs e) - { - if (VirtualView == null) - return; - - // Notify the virtual view about navigation completed - var result = e.Success ? WebNavigationResult.Success : WebNavigationResult.Failure; - VirtualView.Navigated(WebNavigationEvent.NewPage, e.Url, result); - } - - #region Property Mappers - - public static void MapSource(WebViewHandler handler, IWebView webView) - { - var source = webView.Source; - if (source == null) - return; - - Console.WriteLine($"[WebViewHandler] MapSource: {source.GetType().Name}"); - - if (source is IUrlWebViewSource urlSource && !string.IsNullOrEmpty(urlSource.Url)) - { - handler.PlatformView?.LoadUrl(urlSource.Url); - } - else if (source is IHtmlWebViewSource htmlSource && !string.IsNullOrEmpty(htmlSource.Html)) - { - handler.PlatformView?.LoadHtml(htmlSource.Html, htmlSource.BaseUrl); - } - } - - public static void MapUserAgent(WebViewHandler handler, IWebView webView) - { - if (handler.PlatformView != null && !string.IsNullOrEmpty(webView.UserAgent)) - { - handler.PlatformView.UserAgent = webView.UserAgent; - Console.WriteLine($"[WebViewHandler] MapUserAgent: {webView.UserAgent}"); - } - } - - #endregion - - #region Command Mappers - - public static void MapGoBack(WebViewHandler handler, IWebView webView, object? args) - { - if (handler.PlatformView?.CanGoBack == true) - { - handler.PlatformView.GoBack(); - Console.WriteLine("[WebViewHandler] GoBack"); - } - } - - public static void MapGoForward(WebViewHandler handler, IWebView webView, object? args) - { - if (handler.PlatformView?.CanGoForward == true) - { - handler.PlatformView.GoForward(); - Console.WriteLine("[WebViewHandler] GoForward"); - } - } - - public static void MapReload(WebViewHandler handler, IWebView webView, object? args) - { - handler.PlatformView?.Reload(); - Console.WriteLine("[WebViewHandler] Reload"); - } - - public static void MapEval(WebViewHandler handler, IWebView webView, object? args) - { - if (args is string script) - { - handler.PlatformView?.Eval(script); - Console.WriteLine($"[WebViewHandler] Eval: {script.Substring(0, Math.Min(50, script.Length))}..."); - } - } - - public static void MapEvaluateJavaScriptAsync(WebViewHandler handler, IWebView webView, object? args) - { - if (args is EvaluateJavaScriptAsyncRequest request) - { - var result = handler.PlatformView?.EvaluateJavaScriptAsync(request.Script); - if (result != null) - { - result.ContinueWith(t => - { - request.SetResult(t.Result); - }); - } - else - { - request.SetResult(null); - } - Console.WriteLine($"[WebViewHandler] EvaluateJavaScriptAsync: {request.Script.Substring(0, Math.Min(50, request.Script.Length))}..."); - } - } - - #endregion -} - -/// -/// Request object for async JavaScript evaluation. -/// -public class EvaluateJavaScriptAsyncRequest -{ - public string Script { get; } - private readonly TaskCompletionSource _tcs = new(); - - public EvaluateJavaScriptAsyncRequest(string script) - { - Script = script; - } - - public Task Task => _tcs.Task; - - public void SetResult(string? result) - { - _tcs.TrySetResult(result); - } -} diff --git a/Handlers/WebViewHandler.cs b/Handlers/WebViewHandler.cs index 611eaa7..d4e5e79 100644 --- a/Handlers/WebViewHandler.cs +++ b/Handlers/WebViewHandler.cs @@ -1,120 +1,96 @@ -using System; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using Microsoft.Maui.Controls; using Microsoft.Maui.Handlers; +using Microsoft.Maui.Platform; namespace Microsoft.Maui.Platform.Linux.Handlers; -public class WebViewHandler : ViewHandler +/// +/// Handler for WebView control on Linux using WebKitGTK. +/// +public partial class WebViewHandler : ViewHandler { - public static IPropertyMapper Mapper = (IPropertyMapper)(object)new PropertyMapper((IPropertyMapper[])(object)new IPropertyMapper[1] { (IPropertyMapper)ViewHandler.ViewMapper }) { ["Source"] = MapSource }; + public static IPropertyMapper Mapper = new PropertyMapper(ViewHandler.ViewMapper) + { + [nameof(IWebView.Source)] = MapSource, + }; - public static CommandMapper CommandMapper = new CommandMapper((CommandMapper)(object)ViewHandler.ViewCommandMapper) - { - ["GoBack"] = MapGoBack, - ["GoForward"] = MapGoForward, - ["Reload"] = MapReload - }; + public static CommandMapper CommandMapper = new(ViewHandler.ViewCommandMapper) + { + [nameof(IWebView.GoBack)] = MapGoBack, + [nameof(IWebView.GoForward)] = MapGoForward, + [nameof(IWebView.Reload)] = MapReload, + }; - public WebViewHandler() - : base((IPropertyMapper)(object)Mapper, (CommandMapper)(object)CommandMapper) - { - } + public WebViewHandler() : base(Mapper, CommandMapper) + { + } - public WebViewHandler(IPropertyMapper? mapper = null, CommandMapper? commandMapper = null) - : base((IPropertyMapper)(((object)mapper) ?? ((object)Mapper)), (CommandMapper)(((object)commandMapper) ?? ((object)CommandMapper))) - { - } + public WebViewHandler(IPropertyMapper? mapper = null, CommandMapper? commandMapper = null) + : base(mapper ?? Mapper, commandMapper ?? CommandMapper) + { + } - protected override SkiaWebView CreatePlatformView() - { - return new SkiaWebView(); - } + protected override SkiaWebView CreatePlatformView() + { + return new SkiaWebView(); + } - protected override void ConnectHandler(SkiaWebView platformView) - { - base.ConnectHandler(platformView); - platformView.Navigating += OnNavigating; - platformView.Navigated += OnNavigated; - } + protected override void ConnectHandler(SkiaWebView platformView) + { + base.ConnectHandler(platformView); - protected override void DisconnectHandler(SkiaWebView platformView) - { - platformView.Navigating -= OnNavigating; - platformView.Navigated -= OnNavigated; - base.DisconnectHandler(platformView); - } + platformView.Navigating += OnNavigating; + platformView.Navigated += OnNavigated; + } - private void OnNavigating(object? sender, WebNavigatingEventArgs e) - { - //IL_0017: Unknown result type (might be due to invalid IL or missing references) - //IL_001d: Expected O, but got Unknown - IWebView virtualView = base.VirtualView; - IWebViewController val = (IWebViewController)(object)((virtualView is IWebViewController) ? virtualView : null); - if (val != null) - { - WebNavigatingEventArgs e2 = new WebNavigatingEventArgs((WebNavigationEvent)3, (WebViewSource)null, e.Url); - val.SendNavigating(e2); - } - } + protected override void DisconnectHandler(SkiaWebView platformView) + { + platformView.Navigating -= OnNavigating; + platformView.Navigated -= OnNavigated; - private void OnNavigated(object? sender, WebNavigatedEventArgs e) - { - //IL_001b: Unknown result type (might be due to invalid IL or missing references) - //IL_0024: Unknown result type (might be due to invalid IL or missing references) - //IL_0025: Unknown result type (might be due to invalid IL or missing references) - //IL_002b: Expected O, but got Unknown - IWebView virtualView = base.VirtualView; - IWebViewController val = (IWebViewController)(object)((virtualView is IWebViewController) ? virtualView : null); - if (val != null) - { - WebNavigationResult val2 = (WebNavigationResult)(e.Success ? 1 : 4); - WebNavigatedEventArgs e2 = new WebNavigatedEventArgs((WebNavigationEvent)3, (WebViewSource)null, e.Url, val2); - val.SendNavigated(e2); - } - } + base.DisconnectHandler(platformView); + } - public static void MapSource(WebViewHandler handler, IWebView webView) - { - Console.WriteLine("[WebViewHandler] MapSource called"); - if (((ViewHandler)(object)handler).PlatformView == null) - { - Console.WriteLine("[WebViewHandler] PlatformView is null!"); - return; - } - IWebViewSource source = webView.Source; - Console.WriteLine("[WebViewHandler] Source type: " + (((object)source)?.GetType().Name ?? "null")); - UrlWebViewSource val = (UrlWebViewSource)(object)((source is UrlWebViewSource) ? source : null); - if (val != null) - { - Console.WriteLine("[WebViewHandler] Loading URL: " + val.Url); - ((ViewHandler)(object)handler).PlatformView.Source = val.Url ?? ""; - return; - } - HtmlWebViewSource val2 = (HtmlWebViewSource)(object)((source is HtmlWebViewSource) ? source : null); - if (val2 != null) - { - Console.WriteLine($"[WebViewHandler] Loading HTML ({val2.Html?.Length ?? 0} chars)"); - Console.WriteLine("[WebViewHandler] HTML preview: " + val2.Html?.Substring(0, Math.Min(100, val2.Html?.Length ?? 0)) + "..."); - ((ViewHandler)(object)handler).PlatformView.Html = val2.Html ?? ""; - } - else - { - Console.WriteLine("[WebViewHandler] Unknown source type or null"); - } - } + private void OnNavigating(object? sender, WebNavigatingEventArgs e) + { + // Forward to virtual view if needed + } - public static void MapGoBack(WebViewHandler handler, IWebView webView, object? args) - { - ((ViewHandler)(object)handler).PlatformView?.GoBack(); - } + private void OnNavigated(object? sender, WebNavigatedEventArgs e) + { + // Forward to virtual view if needed + } - public static void MapGoForward(WebViewHandler handler, IWebView webView, object? args) - { - ((ViewHandler)(object)handler).PlatformView?.GoForward(); - } + public static void MapSource(WebViewHandler handler, IWebView webView) + { + if (handler.PlatformView == null) return; - public static void MapReload(WebViewHandler handler, IWebView webView, object? args) - { - ((ViewHandler)(object)handler).PlatformView?.Reload(); - } + var source = webView.Source; + if (source is UrlWebViewSource urlSource) + { + handler.PlatformView.Source = urlSource.Url ?? ""; + } + else if (source is HtmlWebViewSource htmlSource) + { + handler.PlatformView.Html = htmlSource.Html ?? ""; + } + } + + public static void MapGoBack(WebViewHandler handler, IWebView webView, object? args) + { + handler.PlatformView?.GoBack(); + } + + public static void MapGoForward(WebViewHandler handler, IWebView webView, object? args) + { + handler.PlatformView?.GoForward(); + } + + public static void MapReload(WebViewHandler handler, IWebView webView, object? args) + { + handler.PlatformView?.Reload(); + } } diff --git a/Handlers/WindowHandler.cs b/Handlers/WindowHandler.cs index 86fc6a6..7dc08aa 100644 --- a/Handlers/WindowHandler.cs +++ b/Handlers/WindowHandler.cs @@ -1,166 +1,281 @@ -using System; -using Microsoft.Maui.Graphics; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using Microsoft.Maui.Handlers; +using Microsoft.Maui.Graphics; +using Microsoft.Maui.Controls; +using Microsoft.Maui.Platform; +using SkiaSharp; namespace Microsoft.Maui.Platform.Linux.Handlers; -public class WindowHandler : ElementHandler +/// +/// Handler for Window on Linux. +/// Maps IWindow to the Linux display window system. +/// +public partial class WindowHandler : ElementHandler { - public static IPropertyMapper Mapper = (IPropertyMapper)(object)new PropertyMapper((IPropertyMapper[])(object)new IPropertyMapper[1] { (IPropertyMapper)ElementHandler.ElementMapper }) - { - ["Title"] = MapTitle, - ["Content"] = MapContent, - ["X"] = MapX, - ["Y"] = MapY, - ["Width"] = MapWidth, - ["Height"] = MapHeight, - ["MinimumWidth"] = MapMinimumWidth, - ["MinimumHeight"] = MapMinimumHeight, - ["MaximumWidth"] = MapMaximumWidth, - ["MaximumHeight"] = MapMaximumHeight - }; + public static IPropertyMapper Mapper = + new PropertyMapper(ElementHandler.ElementMapper) + { + [nameof(IWindow.Title)] = MapTitle, + [nameof(IWindow.Content)] = MapContent, + [nameof(IWindow.X)] = MapX, + [nameof(IWindow.Y)] = MapY, + [nameof(IWindow.Width)] = MapWidth, + [nameof(IWindow.Height)] = MapHeight, + [nameof(IWindow.MinimumWidth)] = MapMinimumWidth, + [nameof(IWindow.MinimumHeight)] = MapMinimumHeight, + [nameof(IWindow.MaximumWidth)] = MapMaximumWidth, + [nameof(IWindow.MaximumHeight)] = MapMaximumHeight, + }; - public static CommandMapper CommandMapper = new CommandMapper((CommandMapper)(object)ElementHandler.ElementCommandMapper); + public static CommandMapper CommandMapper = + new(ElementHandler.ElementCommandMapper) + { + }; - public WindowHandler() - : base((IPropertyMapper)(object)Mapper, (CommandMapper)(object)CommandMapper) - { - } + public WindowHandler() : base(Mapper, CommandMapper) + { + } - public WindowHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null) - : base((IPropertyMapper)(((object)mapper) ?? ((object)Mapper)), (CommandMapper)(((object)commandMapper) ?? ((object)CommandMapper))) - { - } + public WindowHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null) + : base(mapper ?? Mapper, commandMapper ?? CommandMapper) + { + } - protected override SkiaWindow CreatePlatformElement() - { - return new SkiaWindow(); - } + protected override SkiaWindow CreatePlatformElement() + { + return new SkiaWindow(); + } - protected override void ConnectHandler(SkiaWindow platformView) - { - base.ConnectHandler(platformView); - platformView.CloseRequested += OnCloseRequested; - platformView.SizeChanged += OnSizeChanged; - } + protected override void ConnectHandler(SkiaWindow platformView) + { + base.ConnectHandler(platformView); + platformView.CloseRequested += OnCloseRequested; + platformView.SizeChanged += OnSizeChanged; + } - protected override void DisconnectHandler(SkiaWindow platformView) - { - platformView.CloseRequested -= OnCloseRequested; - platformView.SizeChanged -= OnSizeChanged; - base.DisconnectHandler(platformView); - } + protected override void DisconnectHandler(SkiaWindow platformView) + { + platformView.CloseRequested -= OnCloseRequested; + platformView.SizeChanged -= OnSizeChanged; + base.DisconnectHandler(platformView); + } - private void OnCloseRequested(object? sender, EventArgs e) - { - IWindow virtualView = base.VirtualView; - if (virtualView != null) - { - virtualView.Destroying(); - } - } + private void OnCloseRequested(object? sender, EventArgs e) + { + VirtualView?.Destroying(); + } - private void OnSizeChanged(object? sender, SizeChangedEventArgs e) - { - //IL_002b: Unknown result type (might be due to invalid IL or missing references) - IWindow virtualView = base.VirtualView; - if (virtualView != null) - { - virtualView.FrameChanged(new Rect(0.0, 0.0, (double)e.Width, (double)e.Height)); - } - } + private void OnSizeChanged(object? sender, SizeChangedEventArgs e) + { + VirtualView?.FrameChanged(new Rect(0, 0, e.Width, e.Height)); + } - public static void MapTitle(WindowHandler handler, IWindow window) - { - if (((ElementHandler)(object)handler).PlatformView != null) - { - ((ElementHandler)(object)handler).PlatformView.Title = ((ITitledElement)window).Title ?? "MAUI Application"; - } - } + public static void MapTitle(WindowHandler handler, IWindow window) + { + if (handler.PlatformView is null) return; + handler.PlatformView.Title = window.Title ?? "MAUI Application"; + } - public static void MapContent(WindowHandler handler, IWindow window) - { - if (((ElementHandler)(object)handler).PlatformView != null) - { - IView content = window.Content; - object obj; - if (content == null) - { - obj = null; - } - else - { - IViewHandler handler2 = content.Handler; - obj = ((handler2 != null) ? ((IElementHandler)handler2).PlatformView : null); - } - if (obj is SkiaView content2) - { - ((ElementHandler)(object)handler).PlatformView.Content = content2; - } - } - } + public static void MapContent(WindowHandler handler, IWindow window) + { + if (handler.PlatformView is null) return; - public static void MapX(WindowHandler handler, IWindow window) - { - if (((ElementHandler)(object)handler).PlatformView != null) - { - ((ElementHandler)(object)handler).PlatformView.X = (int)window.X; - } - } + var content = window.Content; + if (content?.Handler?.PlatformView is SkiaView skiaContent) + { + handler.PlatformView.Content = skiaContent; + } + } - public static void MapY(WindowHandler handler, IWindow window) - { - if (((ElementHandler)(object)handler).PlatformView != null) - { - ((ElementHandler)(object)handler).PlatformView.Y = (int)window.Y; - } - } + public static void MapX(WindowHandler handler, IWindow window) + { + if (handler.PlatformView is null) return; + handler.PlatformView.X = (int)window.X; + } - public static void MapWidth(WindowHandler handler, IWindow window) - { - if (((ElementHandler)(object)handler).PlatformView != null) - { - ((ElementHandler)(object)handler).PlatformView.Width = (int)window.Width; - } - } + public static void MapY(WindowHandler handler, IWindow window) + { + if (handler.PlatformView is null) return; + handler.PlatformView.Y = (int)window.Y; + } - public static void MapHeight(WindowHandler handler, IWindow window) - { - if (((ElementHandler)(object)handler).PlatformView != null) - { - ((ElementHandler)(object)handler).PlatformView.Height = (int)window.Height; - } - } + public static void MapWidth(WindowHandler handler, IWindow window) + { + if (handler.PlatformView is null) return; + handler.PlatformView.Width = (int)window.Width; + } - public static void MapMinimumWidth(WindowHandler handler, IWindow window) - { - if (((ElementHandler)(object)handler).PlatformView != null) - { - ((ElementHandler)(object)handler).PlatformView.MinWidth = (int)window.MinimumWidth; - } - } + public static void MapHeight(WindowHandler handler, IWindow window) + { + if (handler.PlatformView is null) return; + handler.PlatformView.Height = (int)window.Height; + } - public static void MapMinimumHeight(WindowHandler handler, IWindow window) - { - if (((ElementHandler)(object)handler).PlatformView != null) - { - ((ElementHandler)(object)handler).PlatformView.MinHeight = (int)window.MinimumHeight; - } - } + public static void MapMinimumWidth(WindowHandler handler, IWindow window) + { + if (handler.PlatformView is null) return; + handler.PlatformView.MinWidth = (int)window.MinimumWidth; + } - public static void MapMaximumWidth(WindowHandler handler, IWindow window) - { - if (((ElementHandler)(object)handler).PlatformView != null) - { - ((ElementHandler)(object)handler).PlatformView.MaxWidth = (int)window.MaximumWidth; - } - } + public static void MapMinimumHeight(WindowHandler handler, IWindow window) + { + if (handler.PlatformView is null) return; + handler.PlatformView.MinHeight = (int)window.MinimumHeight; + } - public static void MapMaximumHeight(WindowHandler handler, IWindow window) - { - if (((ElementHandler)(object)handler).PlatformView != null) - { - ((ElementHandler)(object)handler).PlatformView.MaxHeight = (int)window.MaximumHeight; - } - } + public static void MapMaximumWidth(WindowHandler handler, IWindow window) + { + if (handler.PlatformView is null) return; + handler.PlatformView.MaxWidth = (int)window.MaximumWidth; + } + + public static void MapMaximumHeight(WindowHandler handler, IWindow window) + { + if (handler.PlatformView is null) return; + handler.PlatformView.MaxHeight = (int)window.MaximumHeight; + } +} + +/// +/// Skia window wrapper for Linux display servers. +/// Handles rendering of content and popup overlays automatically. +/// +public class SkiaWindow +{ + private SkiaView? _content; + private string _title = "MAUI Application"; + private int _x, _y; + private int _width = 800; + private int _height = 600; + private int _minWidth = 100; + private int _minHeight = 100; + private int _maxWidth = int.MaxValue; + private int _maxHeight = int.MaxValue; + + public SkiaView? Content + { + get => _content; + set + { + _content = value; + ContentChanged?.Invoke(this, EventArgs.Empty); + } + } + + /// + /// Renders the window content and popup overlays to the canvas. + /// This should be called by the platform rendering loop. + /// + public void Render(SKCanvas canvas) + { + // Clear background + canvas.Clear(SKColors.White); + + // Draw main content + if (_content != null) + { + _content.Measure(new SKSize(_width, _height)); + _content.Arrange(new SKRect(0, 0, _width, _height)); + _content.Draw(canvas); + } + + // Draw popup overlays on top (dropdowns, date pickers, etc.) + // This ensures popups always render above all other content + SkiaView.DrawPopupOverlays(canvas); + } + + public string Title + { + get => _title; + set + { + _title = value; + TitleChanged?.Invoke(this, EventArgs.Empty); + } + } + + public int X + { + get => _x; + set { _x = value; PositionChanged?.Invoke(this, EventArgs.Empty); } + } + + public int Y + { + get => _y; + set { _y = value; PositionChanged?.Invoke(this, EventArgs.Empty); } + } + + public int Width + { + get => _width; + set + { + _width = Math.Clamp(value, _minWidth, _maxWidth); + SizeChanged?.Invoke(this, new SizeChangedEventArgs(_width, _height)); + } + } + + public int Height + { + get => _height; + set + { + _height = Math.Clamp(value, _minHeight, _maxHeight); + SizeChanged?.Invoke(this, new SizeChangedEventArgs(_width, _height)); + } + } + + public int MinWidth + { + get => _minWidth; + set { _minWidth = value; } + } + + public int MinHeight + { + get => _minHeight; + set { _minHeight = value; } + } + + public int MaxWidth + { + get => _maxWidth; + set { _maxWidth = value; } + } + + public int MaxHeight + { + get => _maxHeight; + set { _maxHeight = value; } + } + + public event EventHandler? ContentChanged; + public event EventHandler? TitleChanged; + public event EventHandler? PositionChanged; + public event EventHandler? SizeChanged; + public event EventHandler? CloseRequested; + + public void Close() + { + CloseRequested?.Invoke(this, EventArgs.Empty); + } +} + +/// +/// Event args for window size changes. +/// +public class SizeChangedEventArgs : EventArgs +{ + public int Width { get; } + public int Height { get; } + + public SizeChangedEventArgs(int width, int height) + { + Width = width; + Height = height; + } } diff --git a/Hosting/GtkMauiContext.cs b/Hosting/GtkMauiContext.cs deleted file mode 100644 index 29f17f1..0000000 --- a/Hosting/GtkMauiContext.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Maui.Animations; -using Microsoft.Maui.Dispatching; - -namespace Microsoft.Maui.Platform.Linux.Hosting; - -public class GtkMauiContext : IMauiContext -{ - private readonly IServiceProvider _services; - - private readonly IMauiHandlersFactory _handlers; - - private IAnimationManager? _animationManager; - - private IDispatcher? _dispatcher; - - public IServiceProvider Services => _services; - - public IMauiHandlersFactory Handlers => _handlers; - - public IAnimationManager AnimationManager - { - get - { - if (_animationManager == null) - { - _animationManager = (IAnimationManager?)(((object)_services.GetService()) ?? ((object)new LinuxAnimationManager((ITicker)(object)new LinuxTicker()))); - } - return _animationManager; - } - } - - public IDispatcher Dispatcher - { - get - { - if (_dispatcher == null) - { - _dispatcher = (IDispatcher?)(((object)_services.GetService()) ?? ((object)new LinuxDispatcher())); - } - return _dispatcher; - } - } - - public GtkMauiContext(IServiceProvider services) - { - _services = services ?? throw new ArgumentNullException("services"); - _handlers = services.GetRequiredService(); - if (LinuxApplication.Current == null) - { - new LinuxApplication(); - } - } -} diff --git a/Hosting/HandlerMappingExtensions.cs b/Hosting/HandlerMappingExtensions.cs deleted file mode 100644 index b023613..0000000 --- a/Hosting/HandlerMappingExtensions.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Microsoft.Maui.Hosting; - -namespace Microsoft.Maui.Platform.Linux.Hosting; - -public static class HandlerMappingExtensions -{ - public static IMauiHandlersCollection AddHandler(this IMauiHandlersCollection handlers) where TView : class where THandler : class - { - MauiHandlersCollectionExtensions.AddHandler(handlers, typeof(TView), typeof(THandler)); - return handlers; - } -} diff --git a/Hosting/LinuxAnimationManager.cs b/Hosting/LinuxAnimationManager.cs deleted file mode 100644 index c4a4c36..0000000 --- a/Hosting/LinuxAnimationManager.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System.Collections.Generic; -using Microsoft.Maui.Animations; - -namespace Microsoft.Maui.Platform.Linux.Hosting; - -internal class LinuxAnimationManager : IAnimationManager -{ - private readonly List _animations = new List(); - - private readonly ITicker _ticker; - - public double SpeedModifier { get; set; } = 1.0; - - public bool AutoStartTicker { get; set; } = true; - - public ITicker Ticker => _ticker; - - public LinuxAnimationManager(ITicker ticker) - { - _ticker = ticker; - _ticker.Fire = OnTickerFire; - } - - public void Add(Animation animation) - { - _animations.Add(animation); - if (AutoStartTicker && !_ticker.IsRunning) - { - _ticker.Start(); - } - } - - public void Remove(Animation animation) - { - _animations.Remove(animation); - if (_animations.Count == 0 && _ticker.IsRunning) - { - _ticker.Stop(); - } - } - - private void OnTickerFire() - { - Animation[] array = _animations.ToArray(); - foreach (Animation val in array) - { - val.Tick(0.016 * SpeedModifier); - if (val.HasFinished) - { - Remove(val); - } - } - } -} diff --git a/Hosting/LinuxDispatcher.cs b/Hosting/LinuxDispatcher.cs deleted file mode 100644 index 77e363b..0000000 --- a/Hosting/LinuxDispatcher.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Microsoft.Maui.Dispatching; - -namespace Microsoft.Maui.Platform.Linux.Hosting; - -internal class LinuxDispatcher : IDispatcher -{ - private readonly object _lock = new object(); - - private readonly Queue _queue = new Queue(); - - private bool _isDispatching; - - public bool IsDispatchRequired => false; - - public IDispatcherTimer CreateTimer() - { - return (IDispatcherTimer)(object)new LinuxDispatcherTimer(); - } - - public bool Dispatch(Action action) - { - if (action == null) - { - return false; - } - lock (_lock) - { - _queue.Enqueue(action); - } - ProcessQueue(); - return true; - } - - public bool DispatchDelayed(TimeSpan delay, Action action) - { - if (action == null) - { - return false; - } - Task.Delay(delay).ContinueWith((Task _) => Dispatch(action)); - return true; - } - - private void ProcessQueue() - { - if (_isDispatching) - { - return; - } - _isDispatching = true; - try - { - while (true) - { - Action action; - lock (_lock) - { - if (_queue.Count == 0) - { - break; - } - action = _queue.Dequeue(); - } - action?.Invoke(); - } - } - finally - { - _isDispatching = false; - } - } -} diff --git a/Hosting/LinuxDispatcherTimer.cs b/Hosting/LinuxDispatcherTimer.cs deleted file mode 100644 index 5da9e44..0000000 --- a/Hosting/LinuxDispatcherTimer.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System; -using System.Threading; -using Microsoft.Maui.Dispatching; - -namespace Microsoft.Maui.Platform.Linux.Hosting; - -internal class LinuxDispatcherTimer : IDispatcherTimer -{ - private Timer? _timer; - - private TimeSpan _interval = TimeSpan.FromMilliseconds(16L, 0L); - - private bool _isRunning; - - private bool _isRepeating = true; - - public TimeSpan Interval - { - get - { - return _interval; - } - set - { - _interval = value; - } - } - - public bool IsRunning => _isRunning; - - public bool IsRepeating - { - get - { - return _isRepeating; - } - set - { - _isRepeating = value; - } - } - - public event EventHandler? Tick; - - public void Start() - { - if (!_isRunning) - { - _isRunning = true; - _timer = new Timer(OnTimerCallback, null, _interval, _isRepeating ? _interval : Timeout.InfiniteTimeSpan); - } - } - - public void Stop() - { - _isRunning = false; - _timer?.Dispose(); - _timer = null; - } - - private void OnTimerCallback(object? state) - { - this.Tick?.Invoke(this, EventArgs.Empty); - if (!_isRepeating) - { - Stop(); - } - } -} diff --git a/Hosting/LinuxMauiAppBuilderExtensions.cs b/Hosting/LinuxMauiAppBuilderExtensions.cs index b53fdd8..baa6339 100644 --- a/Hosting/LinuxMauiAppBuilderExtensions.cs +++ b/Hosting/LinuxMauiAppBuilderExtensions.cs @@ -1,105 +1,153 @@ -using System; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using System.ComponentModel; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Maui.ApplicationModel; using Microsoft.Maui.ApplicationModel.Communication; using Microsoft.Maui.ApplicationModel.DataTransfer; -using Microsoft.Maui.Controls; -using Microsoft.Maui.Devices; -using Microsoft.Maui.Dispatching; using Microsoft.Maui.Hosting; -using Microsoft.Maui.Networking; -using Microsoft.Maui.Platform.Linux.Converters; -using Microsoft.Maui.Platform.Linux.Dispatching; -using Microsoft.Maui.Platform.Linux.Handlers; using Microsoft.Maui.Platform.Linux.Services; +using Microsoft.Maui.Platform.Linux.Converters; using Microsoft.Maui.Storage; +using Microsoft.Maui.Platform.Linux.Handlers; +using Microsoft.Maui.Controls; using SkiaSharp; namespace Microsoft.Maui.Platform.Linux.Hosting; +/// +/// Extension methods for configuring MAUI applications for Linux. +/// public static class LinuxMauiAppBuilderExtensions { - public static MauiAppBuilder UseLinux(this MauiAppBuilder builder) - { - return builder.UseLinux(null); - } + /// + /// Configures the MAUI application to run on Linux. + /// + public static MauiAppBuilder UseLinux(this MauiAppBuilder builder) + { + return builder.UseLinux(configure: null); + } - public static MauiAppBuilder UseLinux(this MauiAppBuilder builder, Action? configure) - { - LinuxApplicationOptions linuxApplicationOptions = new LinuxApplicationOptions(); - configure?.Invoke(linuxApplicationOptions); - builder.Services.TryAddSingleton((IDispatcherProvider)(object)LinuxDispatcherProvider.Instance); - builder.Services.TryAddSingleton((IDeviceInfo)(object)DeviceInfoService.Instance); - builder.Services.TryAddSingleton((IDeviceDisplay)(object)DeviceDisplayService.Instance); - builder.Services.TryAddSingleton((IAppInfo)(object)AppInfoService.Instance); - builder.Services.TryAddSingleton((IConnectivity)(object)ConnectivityService.Instance); - builder.Services.TryAddSingleton(); - builder.Services.TryAddSingleton(); - builder.Services.TryAddSingleton(); - builder.Services.TryAddSingleton(); - builder.Services.TryAddSingleton(); - builder.Services.TryAddSingleton(); - builder.Services.TryAddSingleton(); - builder.Services.TryAddSingleton(); - builder.Services.TryAddSingleton(); - builder.Services.TryAddSingleton(); - builder.Services.TryAddSingleton((IServiceProvider _) => GtkHostService.Instance); - RegisterTypeConverters(); - HandlerMauiAppBuilderExtensions.ConfigureMauiHandlers(builder, (Action)delegate(IMauiHandlersCollection handlers) - { - handlers.AddHandler(); - handlers.AddHandler(); - handlers.AddHandler(); - handlers.AddHandler(); - handlers.AddHandler(); - handlers.AddHandler(); - handlers.AddHandler(); - handlers.AddHandler(); - handlers.AddHandler(); - handlers.AddHandler(); - handlers.AddHandler(); - handlers.AddHandler(); - handlers.AddHandler(); - handlers.AddHandler(); - handlers.AddHandler(); - handlers.AddHandler(); - handlers.AddHandler(); - handlers.AddHandler(); - handlers.AddHandler(); - handlers.AddHandler(); - handlers.AddHandler(); - handlers.AddHandler(); - handlers.AddHandler(); - handlers.AddHandler(); - handlers.AddHandler(); - handlers.AddHandler(); - handlers.AddHandler(); - handlers.AddHandler(); - handlers.AddHandler(); - handlers.AddHandler(); - handlers.AddHandler(); - handlers.AddHandler(); - handlers.AddHandler(); - handlers.AddHandler(); - handlers.AddHandler(); - handlers.AddHandler(); - handlers.AddHandler(); - handlers.AddHandler(); - handlers.AddHandler(); - handlers.AddHandler(); - handlers.AddHandler(); - }); - builder.Services.AddSingleton(linuxApplicationOptions); - return builder; - } + /// + /// Configures the MAUI application to run on Linux with options. + /// + public static MauiAppBuilder UseLinux(this MauiAppBuilder builder, Action? configure) + { + var options = new LinuxApplicationOptions(); + configure?.Invoke(options); - private static void RegisterTypeConverters() - { - TypeDescriptor.AddAttributes(typeof(SKColor), new TypeConverterAttribute(typeof(SKColorTypeConverter))); - TypeDescriptor.AddAttributes(typeof(SKRect), new TypeConverterAttribute(typeof(SKRectTypeConverter))); - TypeDescriptor.AddAttributes(typeof(SKSize), new TypeConverterAttribute(typeof(SKSizeTypeConverter))); - TypeDescriptor.AddAttributes(typeof(SKPoint), new TypeConverterAttribute(typeof(SKPointTypeConverter))); - } + // Register platform services + builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); + + // Register type converters for XAML support + RegisterTypeConverters(); + + // Register Linux-specific handlers + builder.ConfigureMauiHandlers(handlers => + { + // Application handler + handlers.AddHandler(); + + // Core controls + handlers.AddHandler(); + handlers.AddHandler(); + handlers.AddHandler(); + handlers.AddHandler(); + handlers.AddHandler(); + handlers.AddHandler(); + handlers.AddHandler(); + handlers.AddHandler(); + handlers.AddHandler(); + handlers.AddHandler(); + + // Layout controls + handlers.AddHandler(); + handlers.AddHandler(); + handlers.AddHandler(); + handlers.AddHandler(); + handlers.AddHandler(); + handlers.AddHandler(); + handlers.AddHandler(); + handlers.AddHandler(); + handlers.AddHandler(); + handlers.AddHandler(); + + // Picker controls + handlers.AddHandler(); + handlers.AddHandler(); + handlers.AddHandler(); + handlers.AddHandler(); + + // Progress & Activity + handlers.AddHandler(); + handlers.AddHandler(); + + // Image & Graphics + handlers.AddHandler(); + handlers.AddHandler(); + handlers.AddHandler(); + + // Collection Views + handlers.AddHandler(); + handlers.AddHandler(); + + // Pages & Navigation + handlers.AddHandler(); + handlers.AddHandler(); + handlers.AddHandler(); + handlers.AddHandler(); + handlers.AddHandler(); + handlers.AddHandler(); + + // Application & Window + handlers.AddHandler(); + handlers.AddHandler(); + }); + + // Store options for later use + builder.Services.AddSingleton(options); + + return builder; + } + + /// + /// Registers custom type converters for Linux platform. + /// + private static void RegisterTypeConverters() + { + // Register SkiaSharp type converters for XAML styling support + TypeDescriptor.AddAttributes(typeof(SKColor), new TypeConverterAttribute(typeof(SKColorTypeConverter))); + TypeDescriptor.AddAttributes(typeof(SKRect), new TypeConverterAttribute(typeof(SKRectTypeConverter))); + TypeDescriptor.AddAttributes(typeof(SKSize), new TypeConverterAttribute(typeof(SKSizeTypeConverter))); + TypeDescriptor.AddAttributes(typeof(SKPoint), new TypeConverterAttribute(typeof(SKPointTypeConverter))); + } +} + +/// +/// Handler registration extensions. +/// +public static class HandlerMappingExtensions +{ + /// + /// Adds a handler for the specified view type. + /// + public static IMauiHandlersCollection AddHandler( + this IMauiHandlersCollection handlers) + where TView : class + where THandler : class + { + handlers.AddHandler(typeof(TView), typeof(THandler)); + return handlers; + } } diff --git a/Hosting/LinuxMauiContext.cs b/Hosting/LinuxMauiContext.cs index e97349b..995c22b 100644 --- a/Hosting/LinuxMauiContext.cs +++ b/Hosting/LinuxMauiContext.cs @@ -1,56 +1,299 @@ -using System; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using Microsoft.Extensions.DependencyInjection; using Microsoft.Maui.Animations; using Microsoft.Maui.Dispatching; +using Microsoft.Maui.Platform; +using SkiaSharp; namespace Microsoft.Maui.Platform.Linux.Hosting; +/// +/// Linux-specific implementation of IMauiContext. +/// Provides the infrastructure for creating handlers and accessing platform services. +/// public class LinuxMauiContext : IMauiContext { - private readonly IServiceProvider _services; + private readonly IServiceProvider _services; + private readonly IMauiHandlersFactory _handlers; + private readonly LinuxApplication _linuxApp; + private IAnimationManager? _animationManager; + private IDispatcher? _dispatcher; - private readonly IMauiHandlersFactory _handlers; + public LinuxMauiContext(IServiceProvider services, LinuxApplication linuxApp) + { + _services = services ?? throw new ArgumentNullException(nameof(services)); + _linuxApp = linuxApp ?? throw new ArgumentNullException(nameof(linuxApp)); + _handlers = services.GetRequiredService(); + } - private readonly LinuxApplication _linuxApp; + /// + public IServiceProvider Services => _services; - private IAnimationManager? _animationManager; + /// + public IMauiHandlersFactory Handlers => _handlers; - private IDispatcher? _dispatcher; + /// + /// Gets the Linux application instance. + /// + public LinuxApplication LinuxApp => _linuxApp; - public IServiceProvider Services => _services; + /// + /// Gets the animation manager. + /// + public IAnimationManager AnimationManager + { + get + { + _animationManager ??= _services.GetService() + ?? new LinuxAnimationManager(new LinuxTicker()); + return _animationManager; + } + } - public IMauiHandlersFactory Handlers => _handlers; - - public LinuxApplication LinuxApp => _linuxApp; - - public IAnimationManager AnimationManager - { - get - { - if (_animationManager == null) - { - _animationManager = (IAnimationManager?)(((object)_services.GetService()) ?? ((object)new LinuxAnimationManager((ITicker)(object)new LinuxTicker()))); - } - return _animationManager; - } - } - - public IDispatcher Dispatcher - { - get - { - if (_dispatcher == null) - { - _dispatcher = (IDispatcher?)(((object)_services.GetService()) ?? ((object)new LinuxDispatcher())); - } - return _dispatcher; - } - } - - public LinuxMauiContext(IServiceProvider services, LinuxApplication linuxApp) - { - _services = services ?? throw new ArgumentNullException("services"); - _linuxApp = linuxApp ?? throw new ArgumentNullException("linuxApp"); - _handlers = services.GetRequiredService(); - } + /// + /// Gets the dispatcher for UI thread operations. + /// + public IDispatcher Dispatcher + { + get + { + _dispatcher ??= _services.GetService() + ?? new LinuxDispatcher(); + return _dispatcher; + } + } +} + +/// +/// Scoped MAUI context for a specific window or view hierarchy. +/// +public class ScopedLinuxMauiContext : IMauiContext +{ + private readonly LinuxMauiContext _parent; + + public ScopedLinuxMauiContext(LinuxMauiContext parent) + { + _parent = parent ?? throw new ArgumentNullException(nameof(parent)); + } + + public IServiceProvider Services => _parent.Services; + public IMauiHandlersFactory Handlers => _parent.Handlers; +} + +/// +/// Linux dispatcher for UI thread operations. +/// +internal class LinuxDispatcher : IDispatcher +{ + private readonly object _lock = new(); + private readonly Queue _queue = new(); + private bool _isDispatching; + + public bool IsDispatchRequired => false; // Linux uses single-threaded event loop + + public IDispatcherTimer CreateTimer() + { + return new LinuxDispatcherTimer(); + } + + public bool Dispatch(Action action) + { + if (action == null) + return false; + + lock (_lock) + { + _queue.Enqueue(action); + } + + ProcessQueue(); + return true; + } + + public bool DispatchDelayed(TimeSpan delay, Action action) + { + if (action == null) + return false; + + Task.Delay(delay).ContinueWith(_ => Dispatch(action)); + return true; + } + + private void ProcessQueue() + { + if (_isDispatching) + return; + + _isDispatching = true; + try + { + while (true) + { + Action? action; + lock (_lock) + { + if (_queue.Count == 0) + break; + action = _queue.Dequeue(); + } + action?.Invoke(); + } + } + finally + { + _isDispatching = false; + } + } +} + +/// +/// Linux dispatcher timer implementation. +/// +internal class LinuxDispatcherTimer : IDispatcherTimer +{ + private Timer? _timer; + private TimeSpan _interval = TimeSpan.FromMilliseconds(16); // ~60fps default + private bool _isRunning; + private bool _isRepeating = true; + + public TimeSpan Interval + { + get => _interval; + set => _interval = value; + } + + public bool IsRunning => _isRunning; + + public bool IsRepeating + { + get => _isRepeating; + set => _isRepeating = value; + } + + public event EventHandler? Tick; + + public void Start() + { + if (_isRunning) + return; + + _isRunning = true; + _timer = new Timer(OnTimerCallback, null, _interval, _isRepeating ? _interval : Timeout.InfiniteTimeSpan); + } + + public void Stop() + { + _isRunning = false; + _timer?.Dispose(); + _timer = null; + } + + private void OnTimerCallback(object? state) + { + Tick?.Invoke(this, EventArgs.Empty); + + if (!_isRepeating) + { + Stop(); + } + } +} + +/// +/// Linux animation manager. +/// +internal class LinuxAnimationManager : IAnimationManager +{ + private readonly List _animations = new(); + private readonly ITicker _ticker; + + public LinuxAnimationManager(ITicker ticker) + { + _ticker = ticker; + _ticker.Fire = OnTickerFire; + } + + public double SpeedModifier { get; set; } = 1.0; + public bool AutoStartTicker { get; set; } = true; + + public ITicker Ticker => _ticker; + + public void Add(Microsoft.Maui.Animations.Animation animation) + { + _animations.Add(animation); + + if (AutoStartTicker && !_ticker.IsRunning) + { + _ticker.Start(); + } + } + + public void Remove(Microsoft.Maui.Animations.Animation animation) + { + _animations.Remove(animation); + + if (_animations.Count == 0 && _ticker.IsRunning) + { + _ticker.Stop(); + } + } + + private void OnTickerFire() + { + var animations = _animations.ToArray(); + foreach (var animation in animations) + { + animation.Tick(16.0 / 1000.0 * SpeedModifier); // ~60fps + if (animation.HasFinished) + { + Remove(animation); + } + } + } +} + +/// +/// Linux ticker for animation timing. +/// +internal class LinuxTicker : ITicker +{ + private Timer? _timer; + private bool _isRunning; + private int _maxFps = 60; + + public bool IsRunning => _isRunning; + + public bool SystemEnabled => true; + + public int MaxFps + { + get => _maxFps; + set => _maxFps = Math.Max(1, Math.Min(120, value)); + } + + public Action? Fire { get; set; } + + public void Start() + { + if (_isRunning) + return; + + _isRunning = true; + var interval = TimeSpan.FromMilliseconds(1000.0 / _maxFps); + _timer = new Timer(OnTimerCallback, null, TimeSpan.Zero, interval); + } + + public void Stop() + { + _isRunning = false; + _timer?.Dispose(); + _timer = null; + } + + private void OnTimerCallback(object? state) + { + Fire?.Invoke(); + } } diff --git a/Hosting/LinuxProgramHost.cs b/Hosting/LinuxProgramHost.cs index c9d7ffc..54e61ee 100644 --- a/Hosting/LinuxProgramHost.cs +++ b/Hosting/LinuxProgramHost.cs @@ -1,828 +1,563 @@ -using System; -using System.Collections; -using System.Linq; -using System.Reflection; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using Microsoft.Extensions.DependencyInjection; -using Microsoft.Maui.Controls; -using Microsoft.Maui.Controls.Hosting; using Microsoft.Maui.Hosting; -using Microsoft.Maui.Platform.Linux.Services; +using Microsoft.Maui.Controls; +using Microsoft.Maui.Platform; using SkiaSharp; namespace Microsoft.Maui.Platform.Linux.Hosting; +/// +/// Entry point for running MAUI applications on Linux. +/// public static class LinuxProgramHost { - public static void Run(string[] args) where TApp : class, IApplication, new() - { - Run(args, null); - } + /// + /// Runs the MAUI application on Linux. + /// + /// The application type. + /// Command line arguments. + public static void Run(string[] args) where TApp : class, IApplication, new() + { + Run(args, null); + } - public static void Run(string[] args, Action? configure) where TApp : class, IApplication, new() - { - MauiAppBuilder val = MauiApp.CreateBuilder(true); - val.UseLinux(); - configure?.Invoke(val); - AppHostBuilderExtensions.UseMauiApp(val); - MauiApp val2 = val.Build(); - LinuxApplicationOptions linuxApplicationOptions = val2.Services.GetService() ?? new LinuxApplicationOptions(); - ParseCommandLineOptions(args, linuxApplicationOptions); - GtkHostService.Instance.Initialize(linuxApplicationOptions.Title, linuxApplicationOptions.Width, linuxApplicationOptions.Height); - Console.WriteLine("[LinuxProgramHost] GTK initialized for WebView support"); - using LinuxApplication linuxApplication = new LinuxApplication(); - linuxApplication.Initialize(linuxApplicationOptions); - LinuxMauiContext mauiContext = new LinuxMauiContext(val2.Services, linuxApplication); - IApplication service = val2.Services.GetService(); - Application val3 = (Application)(object)((service is Application) ? service : null); - if (val3 != null && Application.Current == null) - { - typeof(Application).GetProperty("Current")?.SetValue(null, val3); - } - SkiaView skiaView = null; - if (service != null) - { - skiaView = RenderApplication(service, mauiContext, linuxApplicationOptions); - } - if (skiaView == null) - { - Console.WriteLine("No application page found. Showing demo UI."); - skiaView = CreateDemoView(); - } - linuxApplication.RootView = skiaView; - linuxApplication.Run(); - } + /// + /// Runs the MAUI application on Linux with additional configuration. + /// + /// The application type. + /// Command line arguments. + /// Optional builder configuration action. + public static void Run(string[] args, Action? configure) where TApp : class, IApplication, new() + { + // Build the MAUI application + var builder = MauiApp.CreateBuilder(); + builder.UseLinux(); + configure?.Invoke(builder); + builder.UseMauiApp(); + var mauiApp = builder.Build(); - private static SkiaView? RenderApplication(IApplication application, LinuxMauiContext mauiContext, LinuxApplicationOptions options) - { - //IL_0052: Unknown result type (might be due to invalid IL or missing references) - //IL_0058: Expected O, but got Unknown - try - { - Application val = (Application)(object)((application is Application) ? application : null); - if (val != null) - { - Page val2 = val.MainPage; - if (val2 == null && application.Windows.Count > 0) - { - IView content = application.Windows[0].Content; - Page val3 = (Page)(object)((content is Page) ? content : null); - if (val3 != null) - { - val2 = val3; - } - } - if (val2 != null) - { - if (val.Windows.Count == 0) - { - Window val4 = new Window(val2); - val.OpenWindow(val4); - if (val.Windows.Count == 0 && typeof(Application).GetField("_windows", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(val) is IList list) - { - list.Add(val4); - } - } - return RenderPage(val2, mauiContext); - } - } - return null; - } - catch (Exception ex) - { - Console.WriteLine("Error rendering application: " + ex.Message); - Console.WriteLine(ex.StackTrace); - return null; - } - } + // Get application options + var options = mauiApp.Services.GetService() + ?? new LinuxApplicationOptions(); + ParseCommandLineOptions(args, options); - private static SkiaView? RenderPage(Page page, LinuxMauiContext mauiContext) - { - return new LinuxViewRenderer((IMauiContext)(object)mauiContext).RenderPage(page); - } + // Create Linux application + using var linuxApp = new LinuxApplication(); + linuxApp.Initialize(options); - private static void ParseCommandLineOptions(string[] args, LinuxApplicationOptions options) - { - for (int i = 0; i < args.Length; i++) - { - switch (args[i].ToLowerInvariant()) - { - case "--title": - if (i + 1 < args.Length) - { - options.Title = args[++i]; - } - break; - case "--width": - { - if (i + 1 < args.Length && int.TryParse(args[i + 1], out var result2)) - { - options.Width = result2; - i++; - } - break; - } - case "--height": - { - if (i + 1 < args.Length && int.TryParse(args[i + 1], out var result)) - { - options.Height = result; - i++; - } - break; - } - case "--demo": - options.ForceDemo = true; - break; - } - } - } + // Create MAUI context + var mauiContext = new LinuxMauiContext(mauiApp.Services, linuxApp); - public static SkiaView CreateDemoView() - { - //IL_0033: Unknown result type (might be due to invalid IL or missing references) - //IL_0053: Unknown result type (might be due to invalid IL or missing references) - //IL_0080: Unknown result type (might be due to invalid IL or missing references) - //IL_00b3: Unknown result type (might be due to invalid IL or missing references) - //IL_0112: Unknown result type (might be due to invalid IL or missing references) - //IL_013e: Unknown result type (might be due to invalid IL or missing references) - //IL_0171: Unknown result type (might be due to invalid IL or missing references) - //IL_01ad: Unknown result type (might be due to invalid IL or missing references) - //IL_022a: Unknown result type (might be due to invalid IL or missing references) - //IL_023a: Unknown result type (might be due to invalid IL or missing references) - //IL_0297: Unknown result type (might be due to invalid IL or missing references) - //IL_02a3: Unknown result type (might be due to invalid IL or missing references) - //IL_02de: Unknown result type (might be due to invalid IL or missing references) - //IL_02ea: Unknown result type (might be due to invalid IL or missing references) - //IL_0393: Unknown result type (might be due to invalid IL or missing references) - //IL_041f: Unknown result type (might be due to invalid IL or missing references) - //IL_0814: Unknown result type (might be due to invalid IL or missing references) - //IL_088c: Unknown result type (might be due to invalid IL or missing references) - //IL_093a: Unknown result type (might be due to invalid IL or missing references) - //IL_09ee: Unknown result type (might be due to invalid IL or missing references) - //IL_0a97: Unknown result type (might be due to invalid IL or missing references) - //IL_0b18: Unknown result type (might be due to invalid IL or missing references) - //IL_0b32: Unknown result type (might be due to invalid IL or missing references) - //IL_0b6e: Unknown result type (might be due to invalid IL or missing references) - //IL_0ba9: Unknown result type (might be due to invalid IL or missing references) - //IL_0c86: Unknown result type (might be due to invalid IL or missing references) - //IL_0d0d: Unknown result type (might be due to invalid IL or missing references) - //IL_0d32: Unknown result type (might be due to invalid IL or missing references) - //IL_0d78: Unknown result type (might be due to invalid IL or missing references) - //IL_0daa: Unknown result type (might be due to invalid IL or missing references) - //IL_0e60: Unknown result type (might be due to invalid IL or missing references) - //IL_0ea8: Unknown result type (might be due to invalid IL or missing references) - //IL_0edb: Unknown result type (might be due to invalid IL or missing references) - SkiaScrollView skiaScrollView = new SkiaScrollView(); - SkiaStackLayout skiaStackLayout = new SkiaStackLayout - { - Orientation = StackOrientation.Vertical, - Spacing = 15f, - BackgroundColor = new SKColor((byte)245, (byte)245, (byte)245) - }; - skiaStackLayout.Padding = new SKRect(20f, 20f, 20f, 20f); - skiaStackLayout.AddChild(new SkiaLabel - { - Text = "OpenMaui Linux Control Demo", - FontSize = 28f, - TextColor = new SKColor((byte)26, (byte)35, (byte)126), - IsBold = true - }); - skiaStackLayout.AddChild(new SkiaLabel - { - Text = "All controls rendered using SkiaSharp on X11", - FontSize = 14f, - TextColor = SKColors.Gray - }); - skiaStackLayout.AddChild(CreateSeparator()); - skiaStackLayout.AddChild(CreateSectionHeader("Labels")); - SkiaStackLayout skiaStackLayout2 = new SkiaStackLayout - { - Orientation = StackOrientation.Vertical, - Spacing = 5f - }; - skiaStackLayout2.AddChild(new SkiaLabel - { - Text = "Normal Label", - FontSize = 16f, - TextColor = SKColors.Black - }); - skiaStackLayout2.AddChild(new SkiaLabel - { - Text = "Bold Label", - FontSize = 16f, - TextColor = SKColors.Black, - IsBold = true - }); - skiaStackLayout2.AddChild(new SkiaLabel - { - Text = "Italic Label", - FontSize = 16f, - TextColor = SKColors.Gray, - IsItalic = true - }); - skiaStackLayout2.AddChild(new SkiaLabel - { - Text = "Colored Label (Pink)", - FontSize = 16f, - TextColor = new SKColor((byte)233, (byte)30, (byte)99) - }); - skiaStackLayout.AddChild(skiaStackLayout2); - skiaStackLayout.AddChild(CreateSeparator()); - skiaStackLayout.AddChild(CreateSectionHeader("Buttons")); - SkiaStackLayout skiaStackLayout3 = new SkiaStackLayout - { - Orientation = StackOrientation.Horizontal, - Spacing = 10f - }; - SkiaButton btnPrimary = new SkiaButton - { - Text = "Primary", - FontSize = 14f - }; - btnPrimary.BackgroundColor = new SKColor((byte)33, (byte)150, (byte)243); - btnPrimary.TextColor = SKColors.White; - int clickCount = 0; - btnPrimary.Clicked += delegate - { - clickCount++; - btnPrimary.Text = $"Clicked {clickCount}x"; - }; - skiaStackLayout3.AddChild(btnPrimary); - SkiaButton skiaButton = new SkiaButton - { - Text = "Success", - FontSize = 14f - }; - skiaButton.BackgroundColor = new SKColor((byte)76, (byte)175, (byte)80); - skiaButton.TextColor = SKColors.White; - skiaStackLayout3.AddChild(skiaButton); - SkiaButton skiaButton2 = new SkiaButton - { - Text = "Danger", - FontSize = 14f - }; - skiaButton2.BackgroundColor = new SKColor((byte)244, (byte)67, (byte)54); - skiaButton2.TextColor = SKColors.White; - skiaStackLayout3.AddChild(skiaButton2); - skiaStackLayout.AddChild(skiaStackLayout3); - skiaStackLayout.AddChild(CreateSeparator()); - skiaStackLayout.AddChild(CreateSectionHeader("Text Entry")); - SkiaEntry child = new SkiaEntry - { - Placeholder = "Type here...", - FontSize = 14f - }; - skiaStackLayout.AddChild(child); - skiaStackLayout.AddChild(CreateSeparator()); - skiaStackLayout.AddChild(CreateSectionHeader("SearchBar")); - SkiaSearchBar searchBar = new SkiaSearchBar - { - Placeholder = "Search for items..." - }; - SkiaLabel searchResultLabel = new SkiaLabel - { - Text = "", - FontSize = 12f, - TextColor = SKColors.Gray - }; - searchBar.TextChanged += delegate(object? s, TextChangedEventArgs e) - { - searchResultLabel.Text = "Searching: " + e.NewTextValue; - }; - searchBar.SearchButtonPressed += delegate - { - searchResultLabel.Text = "Search submitted: " + searchBar.Text; - }; - skiaStackLayout.AddChild(searchBar); - skiaStackLayout.AddChild(searchResultLabel); - skiaStackLayout.AddChild(CreateSeparator()); - skiaStackLayout.AddChild(CreateSectionHeader("Editor (Multi-line)")); - SkiaEditor child2 = new SkiaEditor - { - Placeholder = "Enter multiple lines of text...", - FontSize = 14f, - BackgroundColor = SKColors.White - }; - skiaStackLayout.AddChild(child2); - skiaStackLayout.AddChild(CreateSeparator()); - skiaStackLayout.AddChild(CreateSectionHeader("CheckBox")); - SkiaStackLayout skiaStackLayout4 = new SkiaStackLayout - { - Orientation = StackOrientation.Horizontal, - Spacing = 20f - }; - SkiaCheckBox child3 = new SkiaCheckBox - { - IsChecked = true - }; - skiaStackLayout4.AddChild(child3); - skiaStackLayout4.AddChild(new SkiaLabel - { - Text = "Checked", - FontSize = 14f - }); - SkiaCheckBox child4 = new SkiaCheckBox - { - IsChecked = false - }; - skiaStackLayout4.AddChild(child4); - skiaStackLayout4.AddChild(new SkiaLabel - { - Text = "Unchecked", - FontSize = 14f - }); - skiaStackLayout.AddChild(skiaStackLayout4); - skiaStackLayout.AddChild(CreateSeparator()); - skiaStackLayout.AddChild(CreateSectionHeader("Switch")); - SkiaStackLayout skiaStackLayout5 = new SkiaStackLayout - { - Orientation = StackOrientation.Horizontal, - Spacing = 20f - }; - SkiaSwitch child5 = new SkiaSwitch - { - IsOn = true - }; - skiaStackLayout5.AddChild(child5); - skiaStackLayout5.AddChild(new SkiaLabel - { - Text = "On", - FontSize = 14f - }); - SkiaSwitch child6 = new SkiaSwitch - { - IsOn = false - }; - skiaStackLayout5.AddChild(child6); - skiaStackLayout5.AddChild(new SkiaLabel - { - Text = "Off", - FontSize = 14f - }); - skiaStackLayout.AddChild(skiaStackLayout5); - skiaStackLayout.AddChild(CreateSeparator()); - skiaStackLayout.AddChild(CreateSectionHeader("RadioButton")); - SkiaStackLayout skiaStackLayout6 = new SkiaStackLayout - { - Orientation = StackOrientation.Horizontal, - Spacing = 15f - }; - skiaStackLayout6.AddChild(new SkiaRadioButton - { - Content = "Option A", - IsChecked = true, - GroupName = "demo" - }); - skiaStackLayout6.AddChild(new SkiaRadioButton - { - Content = "Option B", - IsChecked = false, - GroupName = "demo" - }); - skiaStackLayout6.AddChild(new SkiaRadioButton - { - Content = "Option C", - IsChecked = false, - GroupName = "demo" - }); - skiaStackLayout.AddChild(skiaStackLayout6); - skiaStackLayout.AddChild(CreateSeparator()); - skiaStackLayout.AddChild(CreateSectionHeader("Slider")); - SkiaLabel sliderLabel = new SkiaLabel - { - Text = "Value: 50", - FontSize = 14f - }; - SkiaSlider slider = new SkiaSlider - { - Minimum = 0.0, - Maximum = 100.0, - Value = 50.0 - }; - slider.ValueChanged += delegate - { - sliderLabel.Text = $"Value: {(int)slider.Value}"; - }; - skiaStackLayout.AddChild(slider); - skiaStackLayout.AddChild(sliderLabel); - skiaStackLayout.AddChild(CreateSeparator()); - skiaStackLayout.AddChild(CreateSectionHeader("Stepper")); - SkiaStackLayout skiaStackLayout7 = new SkiaStackLayout - { - Orientation = StackOrientation.Horizontal, - Spacing = 10f - }; - SkiaLabel stepperLabel = new SkiaLabel - { - Text = "Value: 5", - FontSize = 14f - }; - SkiaStepper stepper = new SkiaStepper - { - Value = 5.0, - Minimum = 0.0, - Maximum = 10.0, - Increment = 1.0 - }; - stepper.ValueChanged += delegate - { - stepperLabel.Text = $"Value: {(int)stepper.Value}"; - }; - skiaStackLayout7.AddChild(stepper); - skiaStackLayout7.AddChild(stepperLabel); - skiaStackLayout.AddChild(skiaStackLayout7); - skiaStackLayout.AddChild(CreateSeparator()); - skiaStackLayout.AddChild(CreateSectionHeader("ProgressBar")); - SkiaProgressBar child7 = new SkiaProgressBar - { - Progress = 0.699999988079071 - }; - skiaStackLayout.AddChild(child7); - skiaStackLayout.AddChild(new SkiaLabel - { - Text = "70% Complete", - FontSize = 12f, - TextColor = SKColors.Gray - }); - skiaStackLayout.AddChild(CreateSeparator()); - skiaStackLayout.AddChild(CreateSectionHeader("ActivityIndicator")); - SkiaStackLayout skiaStackLayout8 = new SkiaStackLayout - { - Orientation = StackOrientation.Horizontal, - Spacing = 10f - }; - SkiaActivityIndicator child8 = new SkiaActivityIndicator - { - IsRunning = true - }; - skiaStackLayout8.AddChild(child8); - skiaStackLayout8.AddChild(new SkiaLabel - { - Text = "Loading...", - FontSize = 14f, - TextColor = SKColors.Gray - }); - skiaStackLayout.AddChild(skiaStackLayout8); - skiaStackLayout.AddChild(CreateSeparator()); - skiaStackLayout.AddChild(CreateSectionHeader("Picker (Dropdown)")); - SkiaPicker picker = new SkiaPicker - { - Title = "Select an item" - }; - picker.SetItems(new string[7] { "Apple", "Banana", "Cherry", "Date", "Elderberry", "Fig", "Grape" }); - SkiaLabel pickerLabel = new SkiaLabel - { - Text = "Selected: (none)", - FontSize = 12f, - TextColor = SKColors.Gray - }; - picker.SelectedIndexChanged += delegate - { - pickerLabel.Text = "Selected: " + picker.SelectedItem; - }; - skiaStackLayout.AddChild(picker); - skiaStackLayout.AddChild(pickerLabel); - skiaStackLayout.AddChild(CreateSeparator()); - skiaStackLayout.AddChild(CreateSectionHeader("DatePicker")); - SkiaDatePicker datePicker = new SkiaDatePicker - { - Date = DateTime.Today - }; - SkiaLabel dateLabel = new SkiaLabel - { - Text = $"Date: {DateTime.Today:d}", - FontSize = 12f, - TextColor = SKColors.Gray - }; - datePicker.DateSelected += delegate - { - dateLabel.Text = $"Date: {datePicker.Date:d}"; - }; - skiaStackLayout.AddChild(datePicker); - skiaStackLayout.AddChild(dateLabel); - skiaStackLayout.AddChild(CreateSeparator()); - skiaStackLayout.AddChild(CreateSectionHeader("TimePicker")); - SkiaTimePicker timePicker = new SkiaTimePicker(); - SkiaLabel timeLabel = new SkiaLabel - { - Text = $"Time: {DateTime.Now:t}", - FontSize = 12f, - TextColor = SKColors.Gray - }; - timePicker.TimeSelected += delegate - { - timeLabel.Text = $"Time: {DateTime.Today.Add(timePicker.Time):t}"; - }; - skiaStackLayout.AddChild(timePicker); - skiaStackLayout.AddChild(timeLabel); - skiaStackLayout.AddChild(CreateSeparator()); - skiaStackLayout.AddChild(CreateSectionHeader("Border")); - SkiaBorder skiaBorder = new SkiaBorder - { - CornerRadius = 8f, - StrokeThickness = 2f, - Stroke = new SKColor((byte)33, (byte)150, (byte)243), - BackgroundColor = new SKColor((byte)227, (byte)242, (byte)253) - }; - skiaBorder.SetPadding(15f); - skiaBorder.AddChild(new SkiaLabel - { - Text = "Content inside a styled Border", - FontSize = 14f, - TextColor = new SKColor((byte)26, (byte)35, (byte)126) - }); - skiaStackLayout.AddChild(skiaBorder); - skiaStackLayout.AddChild(CreateSeparator()); - skiaStackLayout.AddChild(CreateSectionHeader("Frame (with shadow)")); - SkiaFrame skiaFrame = new SkiaFrame(); - skiaFrame.BackgroundColor = SKColors.White; - skiaFrame.AddChild(new SkiaLabel - { - Text = "Content inside a Frame with shadow effect", - FontSize = 14f - }); - skiaStackLayout.AddChild(skiaFrame); - skiaStackLayout.AddChild(CreateSeparator()); - skiaStackLayout.AddChild(CreateSectionHeader("CollectionView (List)")); - SkiaCollectionView skiaCollectionView = new SkiaCollectionView - { - SelectionMode = SkiaSelectionMode.Single, - Header = "Fruits", - Footer = "End of list" - }; - skiaCollectionView.ItemsSource = new object[8] { "Apple", "Banana", "Cherry", "Date", "Elderberry", "Fig", "Grape", "Honeydew" }; - SkiaLabel collectionLabel = new SkiaLabel - { - Text = "Selected: (none)", - FontSize = 12f, - TextColor = SKColors.Gray - }; - skiaCollectionView.SelectionChanged += delegate(object? s, CollectionSelectionChangedEventArgs e) - { - object value = e.CurrentSelection.FirstOrDefault(); - collectionLabel.Text = $"Selected: {value}"; - }; - skiaStackLayout.AddChild(skiaCollectionView); - skiaStackLayout.AddChild(collectionLabel); - skiaStackLayout.AddChild(CreateSeparator()); - skiaStackLayout.AddChild(CreateSectionHeader("ImageButton")); - SkiaStackLayout skiaStackLayout9 = new SkiaStackLayout - { - Orientation = StackOrientation.Horizontal, - Spacing = 10f - }; - SkiaImageButton skiaImageButton = new SkiaImageButton - { - CornerRadius = 8f, - StrokeColor = new SKColor((byte)33, (byte)150, (byte)243), - StrokeThickness = 1f, - BackgroundColor = new SKColor((byte)227, (byte)242, (byte)253), - PaddingLeft = 10f, - PaddingRight = 10f, - PaddingTop = 10f, - PaddingBottom = 10f - }; - SKBitmap bitmap = CreateStarIcon(32, new SKColor((byte)33, (byte)150, (byte)243)); - skiaImageButton.Bitmap = bitmap; - SkiaLabel imgBtnLabel = new SkiaLabel - { - Text = "Click the star!", - FontSize = 12f, - TextColor = SKColors.Gray - }; - skiaImageButton.Clicked += delegate - { - imgBtnLabel.Text = "Star clicked!"; - }; - skiaStackLayout9.AddChild(skiaImageButton); - skiaStackLayout9.AddChild(imgBtnLabel); - skiaStackLayout.AddChild(skiaStackLayout9); - skiaStackLayout.AddChild(CreateSeparator()); - skiaStackLayout.AddChild(CreateSectionHeader("Image")); - SkiaStackLayout skiaStackLayout10 = new SkiaStackLayout - { - Orientation = StackOrientation.Horizontal, - Spacing = 10f - }; - SkiaImage skiaImage = new SkiaImage(); - SKBitmap bitmap2 = CreateSampleImage(80, 60); - skiaImage.Bitmap = bitmap2; - skiaStackLayout10.AddChild(skiaImage); - skiaStackLayout10.AddChild(new SkiaLabel - { - Text = "Sample generated image", - FontSize = 12f, - TextColor = SKColors.Gray - }); - skiaStackLayout.AddChild(skiaStackLayout10); - skiaStackLayout.AddChild(CreateSeparator()); - skiaStackLayout.AddChild(new SkiaLabel - { - Text = "All 25+ controls are interactive - try them all!", - FontSize = 16f, - TextColor = new SKColor((byte)76, (byte)175, (byte)80), - IsBold = true - }); - skiaStackLayout.AddChild(new SkiaLabel - { - Text = "Scroll down to see more controls", - FontSize = 12f, - TextColor = SKColors.Gray - }); - skiaScrollView.Content = skiaStackLayout; - return skiaScrollView; - } + // Get the MAUI application instance + var application = mauiApp.Services.GetService(); - private static SkiaLabel CreateSectionHeader(string text) - { - //IL_001e: Unknown result type (might be due to invalid IL or missing references) - return new SkiaLabel - { - Text = text, - FontSize = 18f, - TextColor = new SKColor((byte)55, (byte)71, (byte)79), - IsBold = true - }; - } + // Ensure Application.Current is set - required for Shell.Current to work + if (application is Application app && Application.Current == null) + { + // Use reflection to set Current since it has a protected setter + var currentProperty = typeof(Application).GetProperty("Current"); + currentProperty?.SetValue(null, app); + } - private static SkiaView CreateSeparator() - { - //IL_0020: Unknown result type (might be due to invalid IL or missing references) - return new SkiaLabel - { - Text = "", - BackgroundColor = new SKColor((byte)224, (byte)224, (byte)224), - RequestedHeight = 1.0 - }; - } + // Try to render the application's main page + SkiaView? rootView = null; - private static SKBitmap CreateStarIcon(int size, SKColor color) - { - //IL_0003: Unknown result type (might be due to invalid IL or missing references) - //IL_0009: Expected O, but got Unknown - //IL_000a: Unknown result type (might be due to invalid IL or missing references) - //IL_0010: Expected O, but got Unknown - //IL_0011: Unknown result type (might be due to invalid IL or missing references) - //IL_001b: Unknown result type (might be due to invalid IL or missing references) - //IL_0020: Unknown result type (might be due to invalid IL or missing references) - //IL_0021: Unknown result type (might be due to invalid IL or missing references) - //IL_0027: Unknown result type (might be due to invalid IL or missing references) - //IL_002e: Unknown result type (might be due to invalid IL or missing references) - //IL_0036: Expected O, but got Unknown - //IL_0036: Unknown result type (might be due to invalid IL or missing references) - //IL_003c: Expected O, but got Unknown - SKBitmap val = new SKBitmap(size, size, false); - SKCanvas val2 = new SKCanvas(val); - try - { - val2.Clear(SKColors.Transparent); - SKPaint val3 = new SKPaint - { - Color = color, - Style = (SKPaintStyle)0, - IsAntialias = true - }; - try - { - SKPath val4 = new SKPath(); - try - { - float num = (float)size / 2f; - float num2 = (float)size / 2f; - float num3 = (float)size / 2f - 2f; - float num4 = num3 * 0.4f; - for (int i = 0; i < 5; i++) - { - double num5 = (double)(i * 72 - 90) * Math.PI / 180.0; - double num6 = (double)(i * 72 + 36 - 90) * Math.PI / 180.0; - float num7 = num + num3 * (float)Math.Cos(num5); - float num8 = num2 + num3 * (float)Math.Sin(num5); - float num9 = num + num4 * (float)Math.Cos(num6); - float num10 = num2 + num4 * (float)Math.Sin(num6); - if (i == 0) - { - val4.MoveTo(num7, num8); - } - else - { - val4.LineTo(num7, num8); - } - val4.LineTo(num9, num10); - } - val4.Close(); - val2.DrawPath(val4, val3); - return val; - } - finally - { - ((IDisposable)val4)?.Dispose(); - } - } - finally - { - ((IDisposable)val3)?.Dispose(); - } - } - finally - { - ((IDisposable)val2)?.Dispose(); - } - } + if (application != null) + { + rootView = RenderApplication(application, mauiContext, options); + } - private static SKBitmap CreateSampleImage(int width, int height) - { - //IL_0003: Unknown result type (might be due to invalid IL or missing references) - //IL_0009: Expected O, but got Unknown - //IL_000a: Unknown result type (might be due to invalid IL or missing references) - //IL_0010: Expected O, but got Unknown - //IL_0010: Unknown result type (might be due to invalid IL or missing references) - //IL_0016: Expected O, but got Unknown - //IL_0020: Unknown result type (might be due to invalid IL or missing references) - //IL_0029: Unknown result type (might be due to invalid IL or missing references) - //IL_0042: Unknown result type (might be due to invalid IL or missing references) - //IL_0047: Unknown result type (might be due to invalid IL or missing references) - //IL_0057: Unknown result type (might be due to invalid IL or missing references) - //IL_005c: Unknown result type (might be due to invalid IL or missing references) - //IL_0092: Unknown result type (might be due to invalid IL or missing references) - //IL_0097: Unknown result type (might be due to invalid IL or missing references) - //IL_00a2: Unknown result type (might be due to invalid IL or missing references) - //IL_00ac: Unknown result type (might be due to invalid IL or missing references) - //IL_00b3: Unknown result type (might be due to invalid IL or missing references) - //IL_00bc: Expected O, but got Unknown - //IL_010f: Unknown result type (might be due to invalid IL or missing references) - //IL_0116: Expected O, but got Unknown - //IL_0118: Unknown result type (might be due to invalid IL or missing references) - //IL_011d: Unknown result type (might be due to invalid IL or missing references) - //IL_011e: Unknown result type (might be due to invalid IL or missing references) - //IL_0128: Unknown result type (might be due to invalid IL or missing references) - //IL_0131: Expected O, but got Unknown - SKBitmap val = new SKBitmap(width, height, false); - SKCanvas val2 = new SKCanvas(val); - try - { - SKPaint val3 = new SKPaint(); - try - { - SKShader val4 = SKShader.CreateLinearGradient(new SKPoint(0f, 0f), new SKPoint((float)width, (float)height), (SKColor[])(object)new SKColor[2] - { - new SKColor((byte)66, (byte)165, (byte)245), - new SKColor((byte)126, (byte)87, (byte)194) - }, new float[2] { 0f, 1f }, (SKShaderTileMode)0); - try - { - val3.Shader = val4; - val2.DrawRect(0f, 0f, (float)width, (float)height, val3); - SKPaint val5 = new SKPaint - { - Color = ((SKColor)(ref SKColors.White)).WithAlpha((byte)180), - Style = (SKPaintStyle)0, - IsAntialias = true - }; - try - { - val2.DrawCircle((float)width * 0.3f, (float)height * 0.4f, 15f, val5); - val2.DrawRect((float)width * 0.5f, (float)height * 0.3f, 20f, 20f, val5); - SKFont val6 = new SKFont(SKTypeface.Default, 12f, 1f, 0f); - try - { - SKPaint val7 = new SKPaint(val6) - { - Color = SKColors.White, - IsAntialias = true - }; - try - { - val2.DrawText("IMG", 10f, (float)(height - 8), val7); - return val; - } - finally - { - ((IDisposable)val7)?.Dispose(); - } - } - finally - { - ((IDisposable)val6)?.Dispose(); - } - } - finally - { - ((IDisposable)val5)?.Dispose(); - } - } - finally - { - ((IDisposable)val4)?.Dispose(); - } - } - finally - { - ((IDisposable)val3)?.Dispose(); - } - } - finally - { - ((IDisposable)val2)?.Dispose(); - } - } + // Fallback to demo if no application view is available + if (rootView == null) + { + Console.WriteLine("No application page found. Showing demo UI."); + rootView = CreateDemoView(); + } + + linuxApp.RootView = rootView; + linuxApp.Run(); + } + + /// + /// Renders the MAUI application and returns the root SkiaView. + /// + private static SkiaView? RenderApplication(IApplication application, LinuxMauiContext mauiContext, LinuxApplicationOptions options) + { + try + { + // For Applications, we need to create a window + if (application is Application app) + { + Page? mainPage = app.MainPage; + + // If no MainPage set, check for windows + if (mainPage == null && application.Windows.Count > 0) + { + var existingWindow = application.Windows[0]; + if (existingWindow.Content is Page page) + { + mainPage = page; + } + } + + if (mainPage != null) + { + // Create a MAUI Window and add it to the application + // This ensures Shell.Current works properly (it reads from Application.Current.Windows[0].Page) + if (app.Windows.Count == 0) + { + var mauiWindow = new Microsoft.Maui.Controls.Window(mainPage); + + // Try OpenWindow first + app.OpenWindow(mauiWindow); + + // If that didn't work, use reflection to add directly to _windows + if (app.Windows.Count == 0) + { + var windowsField = typeof(Application).GetField("_windows", + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + if (windowsField?.GetValue(app) is System.Collections.IList windowsList) + { + windowsList.Add(mauiWindow); + } + } + } + + return RenderPage(mainPage, mauiContext); + } + } + + return null; + } + catch (Exception ex) + { + Console.WriteLine($"Error rendering application: {ex.Message}"); + Console.WriteLine(ex.StackTrace); + return null; + } + } + + /// + /// Renders a MAUI Page to a SkiaView. + /// + private static SkiaView? RenderPage(Page page, LinuxMauiContext mauiContext) + { + var renderer = new LinuxViewRenderer(mauiContext); + return renderer.RenderPage(page); + } + + private static void ParseCommandLineOptions(string[] args, LinuxApplicationOptions options) + { + for (int i = 0; i < args.Length; i++) + { + switch (args[i].ToLowerInvariant()) + { + case "--title" when i + 1 < args.Length: + options.Title = args[++i]; + break; + case "--width" when i + 1 < args.Length && int.TryParse(args[i + 1], out var w): + options.Width = w; + i++; + break; + case "--height" when i + 1 < args.Length && int.TryParse(args[i + 1], out var h): + options.Height = h; + i++; + break; + case "--demo": + // Force demo mode + options.ForceDemo = true; + break; + } + } + } + + /// + /// Creates a demo view showcasing all controls. + /// + public static SkiaView CreateDemoView() + { + // Create scrollable container + var scroll = new SkiaScrollView(); + + var root = new SkiaStackLayout + { + Orientation = StackOrientation.Vertical, + Spacing = 15, + BackgroundColor = new SKColor(0xF5, 0xF5, 0xF5) + }; + root.Padding = new SKRect(20, 20, 20, 20); + + // ========== TITLE ========== + root.AddChild(new SkiaLabel + { + Text = "OpenMaui Linux Control Demo", + FontSize = 28, + TextColor = new SKColor(0x1A, 0x23, 0x7E), + IsBold = true + }); + root.AddChild(new SkiaLabel + { + Text = "All controls rendered using SkiaSharp on X11", + FontSize = 14, + TextColor = SKColors.Gray + }); + + // ========== LABELS SECTION ========== + root.AddChild(CreateSeparator()); + root.AddChild(CreateSectionHeader("Labels")); + var labelSection = new SkiaStackLayout { Orientation = StackOrientation.Vertical, Spacing = 5 }; + labelSection.AddChild(new SkiaLabel { Text = "Normal Label", FontSize = 16, TextColor = SKColors.Black }); + labelSection.AddChild(new SkiaLabel { Text = "Bold Label", FontSize = 16, TextColor = SKColors.Black, IsBold = true }); + labelSection.AddChild(new SkiaLabel { Text = "Italic Label", FontSize = 16, TextColor = SKColors.Gray, IsItalic = true }); + labelSection.AddChild(new SkiaLabel { Text = "Colored Label (Pink)", FontSize = 16, TextColor = new SKColor(0xE9, 0x1E, 0x63) }); + root.AddChild(labelSection); + + // ========== BUTTONS SECTION ========== + root.AddChild(CreateSeparator()); + root.AddChild(CreateSectionHeader("Buttons")); + var buttonSection = new SkiaStackLayout { Orientation = StackOrientation.Horizontal, Spacing = 10 }; + + var btnPrimary = new SkiaButton { Text = "Primary", FontSize = 14 }; + btnPrimary.BackgroundColor = new SKColor(0x21, 0x96, 0xF3); + btnPrimary.TextColor = SKColors.White; + var clickCount = 0; + btnPrimary.Clicked += (s, e) => { clickCount++; btnPrimary.Text = $"Clicked {clickCount}x"; }; + buttonSection.AddChild(btnPrimary); + + var btnSuccess = new SkiaButton { Text = "Success", FontSize = 14 }; + btnSuccess.BackgroundColor = new SKColor(0x4C, 0xAF, 0x50); + btnSuccess.TextColor = SKColors.White; + buttonSection.AddChild(btnSuccess); + + var btnDanger = new SkiaButton { Text = "Danger", FontSize = 14 }; + btnDanger.BackgroundColor = new SKColor(0xF4, 0x43, 0x36); + btnDanger.TextColor = SKColors.White; + buttonSection.AddChild(btnDanger); + + root.AddChild(buttonSection); + + // ========== ENTRY SECTION ========== + root.AddChild(CreateSeparator()); + root.AddChild(CreateSectionHeader("Text Entry")); + var entry = new SkiaEntry { Placeholder = "Type here...", FontSize = 14 }; + root.AddChild(entry); + + // ========== SEARCHBAR SECTION ========== + root.AddChild(CreateSeparator()); + root.AddChild(CreateSectionHeader("SearchBar")); + var searchBar = new SkiaSearchBar { Placeholder = "Search for items..." }; + var searchResultLabel = new SkiaLabel { Text = "", FontSize = 12, TextColor = SKColors.Gray }; + searchBar.TextChanged += (s, e) => searchResultLabel.Text = $"Searching: {e.NewTextValue}"; + searchBar.SearchButtonPressed += (s, e) => searchResultLabel.Text = $"Search submitted: {searchBar.Text}"; + root.AddChild(searchBar); + root.AddChild(searchResultLabel); + + // ========== EDITOR SECTION ========== + root.AddChild(CreateSeparator()); + root.AddChild(CreateSectionHeader("Editor (Multi-line)")); + var editor = new SkiaEditor + { + Placeholder = "Enter multiple lines of text...", + FontSize = 14, + BackgroundColor = SKColors.White + }; + root.AddChild(editor); + + // ========== CHECKBOX SECTION ========== + root.AddChild(CreateSeparator()); + root.AddChild(CreateSectionHeader("CheckBox")); + var checkSection = new SkiaStackLayout { Orientation = StackOrientation.Horizontal, Spacing = 20 }; + var cb1 = new SkiaCheckBox { IsChecked = true }; + checkSection.AddChild(cb1); + checkSection.AddChild(new SkiaLabel { Text = "Checked", FontSize = 14 }); + var cb2 = new SkiaCheckBox { IsChecked = false }; + checkSection.AddChild(cb2); + checkSection.AddChild(new SkiaLabel { Text = "Unchecked", FontSize = 14 }); + root.AddChild(checkSection); + + // ========== SWITCH SECTION ========== + root.AddChild(CreateSeparator()); + root.AddChild(CreateSectionHeader("Switch")); + var switchSection = new SkiaStackLayout { Orientation = StackOrientation.Horizontal, Spacing = 20 }; + var sw1 = new SkiaSwitch { IsOn = true }; + switchSection.AddChild(sw1); + switchSection.AddChild(new SkiaLabel { Text = "On", FontSize = 14 }); + var sw2 = new SkiaSwitch { IsOn = false }; + switchSection.AddChild(sw2); + switchSection.AddChild(new SkiaLabel { Text = "Off", FontSize = 14 }); + root.AddChild(switchSection); + + // ========== RADIOBUTTON SECTION ========== + root.AddChild(CreateSeparator()); + root.AddChild(CreateSectionHeader("RadioButton")); + var radioSection = new SkiaStackLayout { Orientation = StackOrientation.Horizontal, Spacing = 15 }; + radioSection.AddChild(new SkiaRadioButton { Content = "Option A", IsChecked = true, GroupName = "demo" }); + radioSection.AddChild(new SkiaRadioButton { Content = "Option B", IsChecked = false, GroupName = "demo" }); + radioSection.AddChild(new SkiaRadioButton { Content = "Option C", IsChecked = false, GroupName = "demo" }); + root.AddChild(radioSection); + + // ========== SLIDER SECTION ========== + root.AddChild(CreateSeparator()); + root.AddChild(CreateSectionHeader("Slider")); + var sliderLabel = new SkiaLabel { Text = "Value: 50", FontSize = 14 }; + var slider = new SkiaSlider { Minimum = 0, Maximum = 100, Value = 50 }; + slider.ValueChanged += (s, e) => sliderLabel.Text = $"Value: {(int)slider.Value}"; + root.AddChild(slider); + root.AddChild(sliderLabel); + + // ========== STEPPER SECTION ========== + root.AddChild(CreateSeparator()); + root.AddChild(CreateSectionHeader("Stepper")); + var stepperSection = new SkiaStackLayout { Orientation = StackOrientation.Horizontal, Spacing = 10 }; + var stepperLabel = new SkiaLabel { Text = "Value: 5", FontSize = 14 }; + var stepper = new SkiaStepper { Value = 5, Minimum = 0, Maximum = 10, Increment = 1 }; + stepper.ValueChanged += (s, e) => stepperLabel.Text = $"Value: {(int)stepper.Value}"; + stepperSection.AddChild(stepper); + stepperSection.AddChild(stepperLabel); + root.AddChild(stepperSection); + + // ========== PROGRESSBAR SECTION ========== + root.AddChild(CreateSeparator()); + root.AddChild(CreateSectionHeader("ProgressBar")); + var progress = new SkiaProgressBar { Progress = 0.7f }; + root.AddChild(progress); + root.AddChild(new SkiaLabel { Text = "70% Complete", FontSize = 12, TextColor = SKColors.Gray }); + + // ========== ACTIVITYINDICATOR SECTION ========== + root.AddChild(CreateSeparator()); + root.AddChild(CreateSectionHeader("ActivityIndicator")); + var activitySection = new SkiaStackLayout { Orientation = StackOrientation.Horizontal, Spacing = 10 }; + var activity = new SkiaActivityIndicator { IsRunning = true }; + activitySection.AddChild(activity); + activitySection.AddChild(new SkiaLabel { Text = "Loading...", FontSize = 14, TextColor = SKColors.Gray }); + root.AddChild(activitySection); + + // ========== PICKER SECTION ========== + root.AddChild(CreateSeparator()); + root.AddChild(CreateSectionHeader("Picker (Dropdown)")); + var picker = new SkiaPicker { Title = "Select an item" }; + picker.SetItems(new[] { "Apple", "Banana", "Cherry", "Date", "Elderberry", "Fig", "Grape" }); + var pickerLabel = new SkiaLabel { Text = "Selected: (none)", FontSize = 12, TextColor = SKColors.Gray }; + picker.SelectedIndexChanged += (s, e) => pickerLabel.Text = $"Selected: {picker.SelectedItem}"; + root.AddChild(picker); + root.AddChild(pickerLabel); + + // ========== DATEPICKER SECTION ========== + root.AddChild(CreateSeparator()); + root.AddChild(CreateSectionHeader("DatePicker")); + var datePicker = new SkiaDatePicker { Date = DateTime.Today }; + var dateLabel = new SkiaLabel { Text = $"Date: {DateTime.Today:d}", FontSize = 12, TextColor = SKColors.Gray }; + datePicker.DateSelected += (s, e) => dateLabel.Text = $"Date: {datePicker.Date:d}"; + root.AddChild(datePicker); + root.AddChild(dateLabel); + + // ========== TIMEPICKER SECTION ========== + root.AddChild(CreateSeparator()); + root.AddChild(CreateSectionHeader("TimePicker")); + var timePicker = new SkiaTimePicker(); + var timeLabel = new SkiaLabel { Text = $"Time: {DateTime.Now:t}", FontSize = 12, TextColor = SKColors.Gray }; + timePicker.TimeSelected += (s, e) => timeLabel.Text = $"Time: {DateTime.Today.Add(timePicker.Time):t}"; + root.AddChild(timePicker); + root.AddChild(timeLabel); + + // ========== BORDER SECTION ========== + root.AddChild(CreateSeparator()); + root.AddChild(CreateSectionHeader("Border")); + var border = new SkiaBorder + { + CornerRadius = 8, + StrokeThickness = 2, + Stroke = new SKColor(0x21, 0x96, 0xF3), + BackgroundColor = new SKColor(0xE3, 0xF2, 0xFD) + }; + border.SetPadding(15); + border.AddChild(new SkiaLabel { Text = "Content inside a styled Border", FontSize = 14, TextColor = new SKColor(0x1A, 0x23, 0x7E) }); + root.AddChild(border); + + // ========== FRAME SECTION ========== + root.AddChild(CreateSeparator()); + root.AddChild(CreateSectionHeader("Frame (with shadow)")); + var frame = new SkiaFrame(); + frame.BackgroundColor = SKColors.White; + frame.AddChild(new SkiaLabel { Text = "Content inside a Frame with shadow effect", FontSize = 14 }); + root.AddChild(frame); + + // ========== COLLECTIONVIEW SECTION ========== + root.AddChild(CreateSeparator()); + root.AddChild(CreateSectionHeader("CollectionView (List)")); + var collectionView = new SkiaCollectionView + { + SelectionMode = SkiaSelectionMode.Single, + Header = "Fruits", + Footer = "End of list" + }; + collectionView.ItemsSource =(new object[] { "Apple", "Banana", "Cherry", "Date", "Elderberry", "Fig", "Grape", "Honeydew" }); + var collectionLabel = new SkiaLabel { Text = "Selected: (none)", FontSize = 12, TextColor = SKColors.Gray }; + collectionView.SelectionChanged += (s, e) => + { + var selected = e.CurrentSelection.FirstOrDefault(); + collectionLabel.Text = $"Selected: {selected}"; + }; + root.AddChild(collectionView); + root.AddChild(collectionLabel); + + // ========== IMAGEBUTTON SECTION ========== + root.AddChild(CreateSeparator()); + root.AddChild(CreateSectionHeader("ImageButton")); + var imageButtonSection = new SkiaStackLayout { Orientation = StackOrientation.Horizontal, Spacing = 10 }; + + // Create ImageButton with a generated icon (since we don't have image files) + var imgBtn = new SkiaImageButton + { + CornerRadius = 8, + StrokeColor = new SKColor(0x21, 0x96, 0xF3), + StrokeThickness = 1, + BackgroundColor = new SKColor(0xE3, 0xF2, 0xFD), + PaddingLeft = 10, + PaddingRight = 10, + PaddingTop = 10, + PaddingBottom = 10 + }; + // Generate a simple star icon bitmap + var iconBitmap = CreateStarIcon(32, new SKColor(0x21, 0x96, 0xF3)); + imgBtn.Bitmap = iconBitmap; + var imgBtnLabel = new SkiaLabel { Text = "Click the star!", FontSize = 12, TextColor = SKColors.Gray }; + imgBtn.Clicked += (s, e) => imgBtnLabel.Text = "Star clicked!"; + imageButtonSection.AddChild(imgBtn); + imageButtonSection.AddChild(imgBtnLabel); + root.AddChild(imageButtonSection); + + // ========== IMAGE SECTION ========== + root.AddChild(CreateSeparator()); + root.AddChild(CreateSectionHeader("Image")); + var imageSection = new SkiaStackLayout { Orientation = StackOrientation.Horizontal, Spacing = 10 }; + + // Create Image with a generated sample image + var img = new SkiaImage(); + var sampleBitmap = CreateSampleImage(80, 60); + img.Bitmap = sampleBitmap; + imageSection.AddChild(img); + imageSection.AddChild(new SkiaLabel { Text = "Sample generated image", FontSize = 12, TextColor = SKColors.Gray }); + root.AddChild(imageSection); + + // ========== FOOTER ========== + root.AddChild(CreateSeparator()); + root.AddChild(new SkiaLabel + { + Text = "All 25+ controls are interactive - try them all!", + FontSize = 16, + TextColor = new SKColor(0x4C, 0xAF, 0x50), + IsBold = true + }); + root.AddChild(new SkiaLabel + { + Text = "Scroll down to see more controls", + FontSize = 12, + TextColor = SKColors.Gray + }); + + scroll.Content = root; + return scroll; + } + + private static SkiaLabel CreateSectionHeader(string text) + { + return new SkiaLabel + { + Text = text, + FontSize = 18, + TextColor = new SKColor(0x37, 0x47, 0x4F), + IsBold = true + }; + } + + private static SkiaView CreateSeparator() + { + var sep = new SkiaLabel { Text = "", BackgroundColor = new SKColor(0xE0, 0xE0, 0xE0), RequestedHeight = 1 }; + return sep; + } + + private static SKBitmap CreateStarIcon(int size, SKColor color) + { + var bitmap = new SKBitmap(size, size); + using var canvas = new SKCanvas(bitmap); + canvas.Clear(SKColors.Transparent); + + using var paint = new SKPaint + { + Color = color, + Style = SKPaintStyle.Fill, + IsAntialias = true + }; + + // Draw a 5-point star + using var path = new SKPath(); + var cx = size / 2f; + var cy = size / 2f; + var outerRadius = size / 2f - 2; + var innerRadius = outerRadius * 0.4f; + + for (int i = 0; i < 5; i++) + { + var outerAngle = (i * 72 - 90) * Math.PI / 180; + var innerAngle = ((i * 72) + 36 - 90) * Math.PI / 180; + + var ox = cx + outerRadius * (float)Math.Cos(outerAngle); + var oy = cy + outerRadius * (float)Math.Sin(outerAngle); + var ix = cx + innerRadius * (float)Math.Cos(innerAngle); + var iy = cy + innerRadius * (float)Math.Sin(innerAngle); + + if (i == 0) + path.MoveTo(ox, oy); + else + path.LineTo(ox, oy); + + path.LineTo(ix, iy); + } + path.Close(); + canvas.DrawPath(path, paint); + + return bitmap; + } + + private static SKBitmap CreateSampleImage(int width, int height) + { + var bitmap = new SKBitmap(width, height); + using var canvas = new SKCanvas(bitmap); + + // Draw gradient background + using var bgPaint = new SKPaint(); + using var shader = SKShader.CreateLinearGradient( + new SKPoint(0, 0), + new SKPoint(width, height), + new SKColor[] { new SKColor(0x42, 0xA5, 0xF5), new SKColor(0x7E, 0x57, 0xC2) }, + new float[] { 0, 1 }, + SKShaderTileMode.Clamp); + bgPaint.Shader = shader; + canvas.DrawRect(0, 0, width, height, bgPaint); + + // Draw some shapes + using var shapePaint = new SKPaint + { + Color = SKColors.White.WithAlpha(180), + Style = SKPaintStyle.Fill, + IsAntialias = true + }; + canvas.DrawCircle(width * 0.3f, height * 0.4f, 15, shapePaint); + canvas.DrawRect(width * 0.5f, height * 0.3f, 20, 20, shapePaint); + + // Draw "IMG" text + using var font = new SKFont(SKTypeface.Default, 12); + using var textPaint = new SKPaint(font) + { + Color = SKColors.White, + IsAntialias = true + }; + canvas.DrawText("IMG", 10, height - 8, textPaint); + + return bitmap; + } } diff --git a/Hosting/LinuxTicker.cs b/Hosting/LinuxTicker.cs deleted file mode 100644 index 6de30b0..0000000 --- a/Hosting/LinuxTicker.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System; -using System.Threading; -using Microsoft.Maui.Animations; - -namespace Microsoft.Maui.Platform.Linux.Hosting; - -internal class LinuxTicker : ITicker -{ - private Timer? _timer; - - private bool _isRunning; - - private int _maxFps = 60; - - public bool IsRunning => _isRunning; - - public bool SystemEnabled => true; - - public int MaxFps - { - get - { - return _maxFps; - } - set - { - _maxFps = Math.Max(1, Math.Min(120, value)); - } - } - - public Action? Fire { get; set; } - - public void Start() - { - if (!_isRunning) - { - _isRunning = true; - TimeSpan period = TimeSpan.FromMilliseconds(1000.0 / (double)_maxFps); - _timer = new Timer(OnTimerCallback, null, TimeSpan.Zero, period); - } - } - - public void Stop() - { - _isRunning = false; - _timer?.Dispose(); - _timer = null; - } - - private void OnTimerCallback(object? state) - { - Fire?.Invoke(); - } -} diff --git a/Hosting/LinuxViewRenderer.cs b/Hosting/LinuxViewRenderer.cs index 0ef6e4b..a1bac7a 100644 --- a/Hosting/LinuxViewRenderer.cs +++ b/Hosting/LinuxViewRenderer.cs @@ -1,470 +1,497 @@ -using System; -using System.Reflection; -using System.Runtime.CompilerServices; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using Microsoft.Maui.Controls; -using Microsoft.Maui.Graphics; +using Microsoft.Maui.Platform; using SkiaSharp; namespace Microsoft.Maui.Platform.Linux.Hosting; +/// +/// Renders MAUI views to Skia platform views. +/// Handles the conversion of the view hierarchy. +/// public class LinuxViewRenderer { - private readonly IMauiContext _mauiContext; + private readonly IMauiContext _mauiContext; - public static Shell? CurrentMauiShell { get; private set; } + /// + /// Static reference to the current MAUI Shell for navigation support. + /// Used when Shell.Current is not available through normal lifecycle. + /// + public static Shell? CurrentMauiShell { get; private set; } - public static SkiaShell? CurrentSkiaShell { get; private set; } + /// + /// Static reference to the current SkiaShell for navigation updates. + /// + public static SkiaShell? CurrentSkiaShell { get; private set; } - public static LinuxViewRenderer? CurrentRenderer { get; set; } + /// + /// Navigate to a route using the SkiaShell directly. + /// Use this instead of Shell.Current.GoToAsync on Linux. + /// + /// The route to navigate to (e.g., "Buttons" or "//Buttons") + /// True if navigation succeeded + public static bool NavigateToRoute(string route) + { + if (CurrentSkiaShell == null) + { + Console.WriteLine($"[NavigateToRoute] CurrentSkiaShell is null"); + return false; + } - public static bool NavigateToRoute(string route) - { - if (CurrentSkiaShell == null) - { - Console.WriteLine("[NavigateToRoute] CurrentSkiaShell is null"); - return false; - } - string text = route.TrimStart('/'); - Console.WriteLine("[NavigateToRoute] Navigating to: " + text); - for (int i = 0; i < CurrentSkiaShell.Sections.Count; i++) - { - ShellSection shellSection = CurrentSkiaShell.Sections[i]; - if (shellSection.Route.Equals(text, StringComparison.OrdinalIgnoreCase) || shellSection.Title.Equals(text, StringComparison.OrdinalIgnoreCase)) - { - Console.WriteLine($"[NavigateToRoute] Found section {i}: {shellSection.Title}"); - CurrentSkiaShell.NavigateToSection(i); - return true; - } - } - Console.WriteLine("[NavigateToRoute] Route not found: " + text); - return false; - } + // Clean up the route - remove leading // or / + var cleanRoute = route.TrimStart('/'); + Console.WriteLine($"[NavigateToRoute] Navigating to: {cleanRoute}"); - public static bool PushPage(Page page) - { - Console.WriteLine("[PushPage] Pushing page: " + ((object)page).GetType().Name); - if (CurrentSkiaShell == null) - { - Console.WriteLine("[PushPage] CurrentSkiaShell is null"); - return false; - } - if (CurrentRenderer == null) - { - Console.WriteLine("[PushPage] CurrentRenderer is null"); - return false; - } - try - { - SkiaView skiaView = null; - ContentPage val = (ContentPage)(object)((page is ContentPage) ? page : null); - if (val != null && val.Content != null) - { - skiaView = CurrentRenderer.RenderView((IView)(object)val.Content); - } - if (skiaView == null) - { - Console.WriteLine("[PushPage] Failed to render page content"); - return false; - } - if (!(skiaView is SkiaScrollView)) - { - skiaView = new SkiaScrollView - { - Content = skiaView - }; - } - CurrentSkiaShell.PushAsync(skiaView, page.Title ?? "Detail"); - Console.WriteLine("[PushPage] Successfully pushed page"); - return true; - } - catch (Exception ex) - { - Console.WriteLine("[PushPage] Error: " + ex.Message); - return false; - } - } + for (int i = 0; i < CurrentSkiaShell.Sections.Count; i++) + { + var section = CurrentSkiaShell.Sections[i]; + if (section.Route.Equals(cleanRoute, StringComparison.OrdinalIgnoreCase) || + section.Title.Equals(cleanRoute, StringComparison.OrdinalIgnoreCase)) + { + Console.WriteLine($"[NavigateToRoute] Found section {i}: {section.Title}"); + CurrentSkiaShell.NavigateToSection(i); + return true; + } + } - public static bool PopPage() - { - Console.WriteLine("[PopPage] Popping page"); - if (CurrentSkiaShell == null) - { - Console.WriteLine("[PopPage] CurrentSkiaShell is null"); - return false; - } - return CurrentSkiaShell.PopAsync(); - } + Console.WriteLine($"[NavigateToRoute] Route not found: {cleanRoute}"); + return false; + } - public LinuxViewRenderer(IMauiContext mauiContext) - { - _mauiContext = mauiContext ?? throw new ArgumentNullException("mauiContext"); - CurrentRenderer = this; - } + /// + /// Current renderer instance for page rendering. + /// + public static LinuxViewRenderer? CurrentRenderer { get; set; } - public SkiaView? RenderPage(Page page) - { - if (page == null) - { - return null; - } - Shell val = (Shell)(object)((page is Shell) ? page : null); - if (val != null) - { - return RenderShell(val); - } - IViewHandler handler = ((VisualElement)page).Handler; - if (handler != null) - { - ((IElementHandler)handler).DisconnectHandler(); - } - if (((IElement)(object)page).ToHandler(_mauiContext).PlatformView is SkiaView skiaView) - { - ContentPage val2 = (ContentPage)(object)((page is ContentPage) ? page : null); - if (val2 != null && val2.Content != null) - { - SkiaView skiaView2 = RenderView((IView)(object)val2.Content); - if (skiaView is SkiaPage skiaPage && skiaView2 != null) - { - skiaPage.Content = skiaView2; - } - } - return skiaView; - } - return null; - } + /// + /// Pushes a page onto the navigation stack. + /// + /// The page to push + /// True if successful + public static bool PushPage(Page page) + { + Console.WriteLine($"[PushPage] Pushing page: {page.GetType().Name}"); - private SkiaShell RenderShell(Shell shell) - { - //IL_0027: Unknown result type (might be due to invalid IL or missing references) - //IL_002c: Unknown result type (might be due to invalid IL or missing references) - //IL_002e: Unknown result type (might be due to invalid IL or missing references) - //IL_0041: Expected I4, but got Unknown - CurrentMauiShell = shell; - SkiaShell skiaShell = new SkiaShell(); - skiaShell.Title = ((Page)shell).Title ?? "App"; - SkiaShell skiaShell2 = skiaShell; - FlyoutBehavior flyoutBehavior = shell.FlyoutBehavior; - skiaShell2.FlyoutBehavior = (int)flyoutBehavior switch - { - 1 => ShellFlyoutBehavior.Flyout, - 2 => ShellFlyoutBehavior.Locked, - 0 => ShellFlyoutBehavior.Disabled, - _ => ShellFlyoutBehavior.Flyout, - }; - skiaShell.MauiShell = shell; - SkiaShell skiaShell3 = skiaShell; - ApplyShellColors(skiaShell3, shell); - object flyoutHeader = shell.FlyoutHeader; - View val = (View)((flyoutHeader is View) ? flyoutHeader : null); - if (val != null) - { - SkiaView skiaView = RenderView((IView)(object)val); - if (skiaView != null) - { - skiaShell3.FlyoutHeaderView = skiaView; - skiaShell3.FlyoutHeaderHeight = (float)((((VisualElement)val).HeightRequest > 0.0) ? ((VisualElement)val).HeightRequest : 140.0); - } - } - Version version = Assembly.GetEntryAssembly()?.GetName().Version; - skiaShell3.FlyoutFooterText = $"Version {version?.Major ?? 1}.{version?.Minor ?? 0}.{version?.Build ?? 0}"; - foreach (ShellItem item in shell.Items) - { - ProcessShellItem(skiaShell3, item); - } - CurrentSkiaShell = skiaShell3; - skiaShell3.ContentRenderer = CreateShellContentPage; - skiaShell3.ColorRefresher = ApplyShellColors; - shell.Navigated += OnShellNavigated; - shell.Navigating += delegate(object? s, ShellNavigatingEventArgs e) - { - Console.WriteLine($"[Navigation] Navigating: {e.Target}"); - }; - Console.WriteLine($"[Navigation] Shell navigation events subscribed. Sections: {skiaShell3.Sections.Count}"); - for (int num = 0; num < skiaShell3.Sections.Count; num++) - { - Console.WriteLine($"[Navigation] Section {num}: Route='{skiaShell3.Sections[num].Route}', Title='{skiaShell3.Sections[num].Title}'"); - } - return skiaShell3; - } + if (CurrentSkiaShell == null) + { + Console.WriteLine($"[PushPage] CurrentSkiaShell is null"); + return false; + } - private static void ApplyShellColors(SkiaShell skiaShell, Shell shell) - { - //IL_000c: Unknown result type (might be due to invalid IL or missing references) - //IL_0012: Invalid comparison between Unknown and I4 - //IL_00e2: Unknown result type (might be due to invalid IL or missing references) - //IL_00d5: Unknown result type (might be due to invalid IL or missing references) - //IL_0087: Unknown result type (might be due to invalid IL or missing references) - //IL_00aa: Unknown result type (might be due to invalid IL or missing references) - //IL_0105: Unknown result type (might be due to invalid IL or missing references) - //IL_013b: Unknown result type (might be due to invalid IL or missing references) - //IL_0125: Unknown result type (might be due to invalid IL or missing references) - //IL_015e: Unknown result type (might be due to invalid IL or missing references) - //IL_0194: Unknown result type (might be due to invalid IL or missing references) - //IL_0187: Unknown result type (might be due to invalid IL or missing references) - //IL_01b7: Unknown result type (might be due to invalid IL or missing references) - //IL_0236: Unknown result type (might be due to invalid IL or missing references) - //IL_021e: Unknown result type (might be due to invalid IL or missing references) - Application current = Application.Current; - bool flag = current != null && (int)current.UserAppTheme == 2; - Console.WriteLine("[ApplyShellColors] Theme is: " + (flag ? "Dark" : "Light")); - if (shell.FlyoutBackgroundColor != null && shell.FlyoutBackgroundColor != Colors.Transparent) - { - Color flyoutBackgroundColor = shell.FlyoutBackgroundColor; - skiaShell.FlyoutBackgroundColor = new SKColor((byte)(flyoutBackgroundColor.Red * 255f), (byte)(flyoutBackgroundColor.Green * 255f), (byte)(flyoutBackgroundColor.Blue * 255f), (byte)(flyoutBackgroundColor.Alpha * 255f)); - Console.WriteLine($"[ApplyShellColors] FlyoutBackgroundColor from MAUI: {skiaShell.FlyoutBackgroundColor}"); - } - else - { - skiaShell.FlyoutBackgroundColor = (flag ? new SKColor((byte)30, (byte)30, (byte)30) : new SKColor(byte.MaxValue, byte.MaxValue, byte.MaxValue)); - Console.WriteLine($"[ApplyShellColors] Using default FlyoutBackgroundColor: {skiaShell.FlyoutBackgroundColor}"); - } - skiaShell.FlyoutTextColor = (flag ? new SKColor((byte)224, (byte)224, (byte)224) : new SKColor((byte)33, (byte)33, (byte)33)); - Console.WriteLine($"[ApplyShellColors] FlyoutTextColor: {skiaShell.FlyoutTextColor}"); - skiaShell.ContentBackgroundColor = (flag ? new SKColor((byte)18, (byte)18, (byte)18) : new SKColor((byte)250, (byte)250, (byte)250)); - Console.WriteLine($"[ApplyShellColors] ContentBackgroundColor: {skiaShell.ContentBackgroundColor}"); - if (((VisualElement)shell).BackgroundColor != null && ((VisualElement)shell).BackgroundColor != Colors.Transparent) - { - Color backgroundColor = ((VisualElement)shell).BackgroundColor; - skiaShell.NavBarBackgroundColor = new SKColor((byte)(backgroundColor.Red * 255f), (byte)(backgroundColor.Green * 255f), (byte)(backgroundColor.Blue * 255f), (byte)(backgroundColor.Alpha * 255f)); - } - else - { - skiaShell.NavBarBackgroundColor = new SKColor((byte)33, (byte)150, (byte)243); - } - } + if (CurrentRenderer == null) + { + Console.WriteLine($"[PushPage] CurrentRenderer is null"); + return false; + } - private static void OnShellNavigated(object? sender, ShellNavigatedEventArgs e) - { - //IL_0019: Unknown result type (might be due to invalid IL or missing references) - DefaultInterpolatedStringHandler defaultInterpolatedStringHandler = new DefaultInterpolatedStringHandler(70, 3); - defaultInterpolatedStringHandler.AppendLiteral("[Navigation] OnShellNavigated called - Source: "); - defaultInterpolatedStringHandler.AppendFormatted(e.Source); - defaultInterpolatedStringHandler.AppendLiteral(", Current: "); - ShellNavigationState current = e.Current; - defaultInterpolatedStringHandler.AppendFormatted((current != null) ? current.Location : null); - defaultInterpolatedStringHandler.AppendLiteral(", Previous: "); - ShellNavigationState previous = e.Previous; - defaultInterpolatedStringHandler.AppendFormatted((previous != null) ? previous.Location : null); - Console.WriteLine(defaultInterpolatedStringHandler.ToStringAndClear()); - if (CurrentSkiaShell == null || CurrentMauiShell == null) - { - Console.WriteLine("[Navigation] CurrentSkiaShell or CurrentMauiShell is null"); - return; - } - ShellNavigationState currentState = CurrentMauiShell.CurrentState; - string text = ((currentState == null) ? null : currentState.Location?.OriginalString) ?? ""; - Console.WriteLine($"[Navigation] Location: {text}, Sections: {CurrentSkiaShell.Sections.Count}"); - for (int i = 0; i < CurrentSkiaShell.Sections.Count; i++) - { - ShellSection shellSection = CurrentSkiaShell.Sections[i]; - Console.WriteLine($"[Navigation] Checking section {i}: Route='{shellSection.Route}', Title='{shellSection.Title}'"); - if (!string.IsNullOrEmpty(shellSection.Route) && text.Contains(shellSection.Route, StringComparison.OrdinalIgnoreCase)) - { - Console.WriteLine($"[Navigation] Match found by route! Navigating to section {i}"); - if (i != CurrentSkiaShell.CurrentSectionIndex) - { - CurrentSkiaShell.NavigateToSection(i); - } - return; - } - if (!string.IsNullOrEmpty(shellSection.Title) && text.Contains(shellSection.Title, StringComparison.OrdinalIgnoreCase)) - { - Console.WriteLine($"[Navigation] Match found by title! Navigating to section {i}"); - if (i != CurrentSkiaShell.CurrentSectionIndex) - { - CurrentSkiaShell.NavigateToSection(i); - } - return; - } - } - Console.WriteLine("[Navigation] No matching section found for location: " + text); - } + try + { + // Render the page content + SkiaView? pageContent = null; + if (page is ContentPage contentPage && contentPage.Content != null) + { + pageContent = CurrentRenderer.RenderView(contentPage.Content); + } - private void ProcessShellItem(SkiaShell skiaShell, ShellItem item) - { - FlyoutItem val = (FlyoutItem)(object)((item is FlyoutItem) ? item : null); - if (val != null) - { - ShellSection shellSection = new ShellSection - { - Title = (((BaseShellItem)val).Title ?? ""), - Route = (((BaseShellItem)val).Route ?? ((BaseShellItem)val).Title ?? "") - }; - foreach (ShellSection item2 in ((ShellItem)val).Items) - { - foreach (ShellContent item3 in item2.Items) - { - ShellContent shellContent = new ShellContent - { - Title = (((BaseShellItem)item3).Title ?? ((BaseShellItem)item2).Title ?? ((BaseShellItem)val).Title ?? ""), - Route = (((BaseShellItem)item3).Route ?? ""), - MauiShellContent = item3 - }; - SkiaView skiaView = CreateShellContentPage(item3); - if (skiaView != null) - { - shellContent.Content = skiaView; - } - shellSection.Items.Add(shellContent); - } - } - if (shellSection.Items.Count == 1) - { - shellSection.Title = shellSection.Items[0].Title; - } - skiaShell.AddSection(shellSection); - return; - } - TabBar val2 = (TabBar)(object)((item is TabBar) ? item : null); - if (val2 != null) - { - foreach (ShellSection item4 in ((ShellItem)val2).Items) - { - ShellSection shellSection2 = new ShellSection - { - Title = (((BaseShellItem)item4).Title ?? ""), - Route = (((BaseShellItem)item4).Route ?? "") - }; - foreach (ShellContent item5 in item4.Items) - { - ShellContent shellContent2 = new ShellContent - { - Title = (((BaseShellItem)item5).Title ?? ((BaseShellItem)item4).Title ?? ""), - Route = (((BaseShellItem)item5).Route ?? ""), - MauiShellContent = item5 - }; - SkiaView skiaView2 = CreateShellContentPage(item5); - if (skiaView2 != null) - { - shellContent2.Content = skiaView2; - } - shellSection2.Items.Add(shellContent2); - } - skiaShell.AddSection(shellSection2); - } - return; - } - ShellSection shellSection3 = new ShellSection - { - Title = (((BaseShellItem)item).Title ?? ""), - Route = (((BaseShellItem)item).Route ?? "") - }; - foreach (ShellSection item6 in item.Items) - { - foreach (ShellContent item7 in item6.Items) - { - ShellContent shellContent3 = new ShellContent - { - Title = (((BaseShellItem)item7).Title ?? ""), - Route = (((BaseShellItem)item7).Route ?? ""), - MauiShellContent = item7 - }; - SkiaView skiaView3 = CreateShellContentPage(item7); - if (skiaView3 != null) - { - shellContent3.Content = skiaView3; - } - shellSection3.Items.Add(shellContent3); - } - } - skiaShell.AddSection(shellSection3); - } + if (pageContent == null) + { + Console.WriteLine($"[PushPage] Failed to render page content"); + return false; + } - private SkiaView? CreateShellContentPage(ShellContent content) - { - //IL_00bc: Unknown result type (might be due to invalid IL or missing references) - //IL_0135: Unknown result type (might be due to invalid IL or missing references) - //IL_010a: Unknown result type (might be due to invalid IL or missing references) - try - { - Page val = null; - if (content.ContentTemplate != null) - { - object obj = ((ElementTemplate)content.ContentTemplate).CreateContent(); - val = (Page)((obj is Page) ? obj : null); - } - if (val == null) - { - object content2 = content.Content; - Page val2 = (Page)((content2 is Page) ? content2 : null); - if (val2 != null) - { - val = val2; - } - } - ContentPage val3 = (ContentPage)(object)((val is ContentPage) ? val : null); - if (val3 != null && val3.Content != null) - { - SkiaView skiaView = RenderView((IView)(object)val3.Content); - if (skiaView != null) - { - SKColor? value = null; - if (((VisualElement)val3).BackgroundColor != null && ((VisualElement)val3).BackgroundColor != Colors.Transparent) - { - Color backgroundColor = ((VisualElement)val3).BackgroundColor; - value = new SKColor((byte)(backgroundColor.Red * 255f), (byte)(backgroundColor.Green * 255f), (byte)(backgroundColor.Blue * 255f), (byte)(backgroundColor.Alpha * 255f)); - Console.WriteLine($"[CreateShellContentPage] Page BackgroundColor: {value}"); - } - if (skiaView is SkiaScrollView skiaScrollView) - { - if (value.HasValue) - { - skiaScrollView.BackgroundColor = value.Value; - } - return skiaScrollView; - } - SkiaScrollView skiaScrollView2 = new SkiaScrollView - { - Content = skiaView - }; - if (value.HasValue) - { - skiaScrollView2.BackgroundColor = value.Value; - } - return skiaScrollView2; - } - } - } - catch (Exception) - { - } - return null; - } + // Wrap in ScrollView if needed + if (pageContent is not SkiaScrollView) + { + var scrollView = new SkiaScrollView { Content = pageContent }; + pageContent = scrollView; + } - public SkiaView? RenderView(IView view) - { - if (view == null) - { - return null; - } - try - { - Element val = (Element)(object)((view is Element) ? view : null); - if (val != null && val.Handler != null) - { - val.Handler.DisconnectHandler(); - } - IElementHandler obj = ((IElement)(object)view).ToHandler(_mauiContext); - if (!(((obj != null) ? obj.PlatformView : null) is SkiaView result)) - { - return CreateFallbackView(view); - } - return result; - } - catch (Exception) - { - return CreateFallbackView(view); - } - } + // Push onto SkiaShell's navigation stack + CurrentSkiaShell.PushAsync(pageContent, page.Title ?? "Detail"); + Console.WriteLine($"[PushPage] Successfully pushed page"); + return true; + } + catch (Exception ex) + { + Console.WriteLine($"[PushPage] Error: {ex.Message}"); + return false; + } + } - private SkiaView CreateFallbackView(IView view) - { - //IL_0026: Unknown result type (might be due to invalid IL or missing references) - return new SkiaLabel - { - Text = "[" + ((object)view).GetType().Name + "]", - TextColor = SKColors.Gray, - FontSize = 12f - }; - } + /// + /// Pops the current page from the navigation stack. + /// + /// True if successful + public static bool PopPage() + { + Console.WriteLine($"[PopPage] Popping page"); + + if (CurrentSkiaShell == null) + { + Console.WriteLine($"[PopPage] CurrentSkiaShell is null"); + return false; + } + + return CurrentSkiaShell.PopAsync(); + } + + public LinuxViewRenderer(IMauiContext mauiContext) + { + _mauiContext = mauiContext ?? throw new ArgumentNullException(nameof(mauiContext)); + // Store reference for push/pop navigation + CurrentRenderer = this; + } + + /// + /// Renders a MAUI page and returns the corresponding SkiaView. + /// + public SkiaView? RenderPage(Page page) + { + if (page == null) + return null; + + // Special handling for Shell - Shell is our navigation container + if (page is Shell shell) + { + return RenderShell(shell); + } + + // Set handler context + page.Handler?.DisconnectHandler(); + var handler = page.ToHandler(_mauiContext); + + 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; + } + + return null; + } + + /// + /// Renders a MAUI Shell with all its navigation structure. + /// + private SkiaShell RenderShell(Shell shell) + { + // Store reference for navigation - Shell.Current is computed from Application.Current.Windows + // Our platform handles navigation through SkiaShell directly + CurrentMauiShell = shell; + + var skiaShell = new SkiaShell + { + Title = shell.Title ?? "App", + FlyoutBehavior = shell.FlyoutBehavior switch + { + FlyoutBehavior.Flyout => ShellFlyoutBehavior.Flyout, + FlyoutBehavior.Locked => ShellFlyoutBehavior.Locked, + FlyoutBehavior.Disabled => ShellFlyoutBehavior.Disabled, + _ => ShellFlyoutBehavior.Flyout + } + }; + + // Process shell items into sections + foreach (var item in shell.Items) + { + ProcessShellItem(skiaShell, item); + } + + // Store reference to SkiaShell for navigation + CurrentSkiaShell = skiaShell; + + // Subscribe to MAUI Shell navigation events to update SkiaShell + shell.Navigated += OnShellNavigated; + shell.Navigating += (s, e) => Console.WriteLine($"[Navigation] Navigating: {e.Target}"); + + Console.WriteLine($"[Navigation] Shell navigation events subscribed. Sections: {skiaShell.Sections.Count}"); + for (int i = 0; i < skiaShell.Sections.Count; i++) + { + Console.WriteLine($"[Navigation] Section {i}: Route='{skiaShell.Sections[i].Route}', Title='{skiaShell.Sections[i].Title}'"); + } + + return skiaShell; + } + + /// + /// Handles MAUI Shell navigation events and updates SkiaShell accordingly. + /// + private static void OnShellNavigated(object? sender, ShellNavigatedEventArgs e) + { + Console.WriteLine($"[Navigation] OnShellNavigated called - Source: {e.Source}, Current: {e.Current?.Location}, Previous: {e.Previous?.Location}"); + + if (CurrentSkiaShell == null || CurrentMauiShell == null) + { + Console.WriteLine($"[Navigation] CurrentSkiaShell or CurrentMauiShell is null"); + return; + } + + // Get the current route from the Shell + var currentState = CurrentMauiShell.CurrentState; + var location = currentState?.Location?.OriginalString ?? ""; + Console.WriteLine($"[Navigation] Location: {location}, Sections: {CurrentSkiaShell.Sections.Count}"); + + // Find the matching section in SkiaShell by route + for (int i = 0; i < CurrentSkiaShell.Sections.Count; i++) + { + var section = CurrentSkiaShell.Sections[i]; + Console.WriteLine($"[Navigation] Checking section {i}: Route='{section.Route}', Title='{section.Title}'"); + if (!string.IsNullOrEmpty(section.Route) && location.Contains(section.Route, StringComparison.OrdinalIgnoreCase)) + { + Console.WriteLine($"[Navigation] Match found by route! Navigating to section {i}"); + if (i != CurrentSkiaShell.CurrentSectionIndex) + { + CurrentSkiaShell.NavigateToSection(i); + } + return; + } + if (!string.IsNullOrEmpty(section.Title) && location.Contains(section.Title, StringComparison.OrdinalIgnoreCase)) + { + Console.WriteLine($"[Navigation] Match found by title! Navigating to section {i}"); + if (i != CurrentSkiaShell.CurrentSectionIndex) + { + CurrentSkiaShell.NavigateToSection(i); + } + return; + } + } + Console.WriteLine($"[Navigation] No matching section found for location: {location}"); + } + + /// + /// Process a ShellItem (FlyoutItem, TabBar, etc.) into SkiaShell sections. + /// + private void ProcessShellItem(SkiaShell skiaShell, ShellItem item) + { + if (item is FlyoutItem flyoutItem) + { + // Each FlyoutItem becomes a section + var section = new ShellSection + { + Title = flyoutItem.Title ?? "", + Route = flyoutItem.Route ?? flyoutItem.Title ?? "" + }; + + // Process the items within the FlyoutItem + foreach (var shellSection in flyoutItem.Items) + { + foreach (var content in shellSection.Items) + { + var shellContent = new ShellContent + { + Title = content.Title ?? shellSection.Title ?? flyoutItem.Title ?? "", + Route = content.Route ?? "" + }; + + // Create the page content + var pageContent = CreateShellContentPage(content); + if (pageContent != null) + { + shellContent.Content = pageContent; + } + + section.Items.Add(shellContent); + } + } + + // If there's only one item, use it as the main section content + if (section.Items.Count == 1) + { + section.Title = section.Items[0].Title; + } + + skiaShell.AddSection(section); + } + else if (item is TabBar tabBar) + { + // TabBar items get their own sections + foreach (var tab in tabBar.Items) + { + var section = new ShellSection + { + Title = tab.Title ?? "", + Route = tab.Route ?? "" + }; + + foreach (var content in tab.Items) + { + var shellContent = new ShellContent + { + Title = content.Title ?? tab.Title ?? "", + Route = content.Route ?? "" + }; + + var pageContent = CreateShellContentPage(content); + if (pageContent != null) + { + shellContent.Content = pageContent; + } + + section.Items.Add(shellContent); + } + + skiaShell.AddSection(section); + } + } + else + { + // Generic ShellItem + var section = new ShellSection + { + Title = item.Title ?? "", + Route = item.Route ?? "" + }; + + foreach (var shellSection in item.Items) + { + foreach (var content in shellSection.Items) + { + var shellContent = new ShellContent + { + Title = content.Title ?? "", + Route = content.Route ?? "" + }; + + var pageContent = CreateShellContentPage(content); + if (pageContent != null) + { + shellContent.Content = pageContent; + } + + section.Items.Add(shellContent); + } + } + + skiaShell.AddSection(section); + } + } + + /// + /// Creates the page content for a ShellContent. + /// + private SkiaView? CreateShellContentPage(Controls.ShellContent content) + { + try + { + // Try to create the page from the content template + Page? page = null; + + if (content.ContentTemplate != null) + { + page = content.ContentTemplate.CreateContent() as Page; + } + + if (page == null && content.Content is Page contentPage) + { + page = contentPage; + } + + if (page is ContentPage cp && cp.Content != null) + { + // Wrap in a scroll view if not already scrollable + var contentView = RenderView(cp.Content); + if (contentView != null) + { + if (contentView is SkiaScrollView) + { + return contentView; + } + else + { + var scrollView = new SkiaScrollView + { + Content = contentView + }; + return scrollView; + } + } + } + } + catch (Exception) + { + // Silently handle template creation errors + } + + return null; + } + + /// + /// Renders a MAUI view and returns the corresponding SkiaView. + /// + public SkiaView? RenderView(IView view) + { + if (view == null) + return null; + + try + { + // Disconnect any existing handler + if (view is Element element && element.Handler != null) + { + element.Handler.DisconnectHandler(); + } + + // Create handler for the view + // The handler's ConnectHandler and property mappers handle child views automatically + var handler = view.ToHandler(_mauiContext); + + if (handler?.PlatformView is not SkiaView skiaView) + { + // If no Skia handler, create a fallback + return CreateFallbackView(view); + } + + // Handlers manage their own children via ConnectHandler and property mappers + // No manual child rendering needed here - that caused "View already has a parent" errors + return skiaView; + } + catch (Exception) + { + return CreateFallbackView(view); + } + } + + /// + /// Creates a fallback view for unsupported view types. + /// + private SkiaView CreateFallbackView(IView view) + { + // For views without handlers, create a placeholder + return new SkiaLabel + { + Text = $"[{view.GetType().Name}]", + TextColor = SKColors.Gray, + FontSize = 12 + }; + } +} + +/// +/// Extension methods for MAUI handler creation. +/// +public static class MauiHandlerExtensions +{ + /// + /// Creates a handler for the view and returns it. + /// + public static IElementHandler ToHandler(this IElement element, IMauiContext mauiContext) + { + var handler = mauiContext.Handlers.GetHandler(element.GetType()); + if (handler != null) + { + handler.SetMauiContext(mauiContext); + handler.SetVirtualView(element); + } + return handler!; + } } diff --git a/Hosting/MauiHandlerExtensions.cs b/Hosting/MauiHandlerExtensions.cs deleted file mode 100644 index f960b64..0000000 --- a/Hosting/MauiHandlerExtensions.cs +++ /dev/null @@ -1,104 +0,0 @@ -using System; -using System.Collections.Generic; -using Microsoft.Maui.Controls; -using Microsoft.Maui.Platform.Linux.Handlers; - -namespace Microsoft.Maui.Platform.Linux.Hosting; - -public static class MauiHandlerExtensions -{ - private static readonly Dictionary> LinuxHandlerMap = new Dictionary> - { - [typeof(Button)] = () => (IElementHandler)(object)new TextButtonHandler(), - [typeof(Label)] = () => (IElementHandler)(object)new LabelHandler(), - [typeof(Entry)] = () => (IElementHandler)(object)new EntryHandler(), - [typeof(Editor)] = () => (IElementHandler)(object)new EditorHandler(), - [typeof(CheckBox)] = () => (IElementHandler)(object)new CheckBoxHandler(), - [typeof(Switch)] = () => (IElementHandler)(object)new SwitchHandler(), - [typeof(Slider)] = () => (IElementHandler)(object)new SliderHandler(), - [typeof(Stepper)] = () => (IElementHandler)(object)new StepperHandler(), - [typeof(ProgressBar)] = () => (IElementHandler)(object)new ProgressBarHandler(), - [typeof(ActivityIndicator)] = () => (IElementHandler)(object)new ActivityIndicatorHandler(), - [typeof(Picker)] = () => (IElementHandler)(object)new PickerHandler(), - [typeof(DatePicker)] = () => (IElementHandler)(object)new DatePickerHandler(), - [typeof(TimePicker)] = () => (IElementHandler)(object)new TimePickerHandler(), - [typeof(SearchBar)] = () => (IElementHandler)(object)new SearchBarHandler(), - [typeof(RadioButton)] = () => (IElementHandler)(object)new RadioButtonHandler(), - [typeof(WebView)] = () => (IElementHandler)(object)new GtkWebViewHandler(), - [typeof(Image)] = () => (IElementHandler)(object)new ImageHandler(), - [typeof(ImageButton)] = () => (IElementHandler)(object)new ImageButtonHandler(), - [typeof(BoxView)] = () => (IElementHandler)(object)new BoxViewHandler(), - [typeof(Frame)] = () => (IElementHandler)(object)new FrameHandler(), - [typeof(Border)] = () => (IElementHandler)(object)new BorderHandler(), - [typeof(ContentView)] = () => (IElementHandler)(object)new BorderHandler(), - [typeof(ScrollView)] = () => (IElementHandler)(object)new ScrollViewHandler(), - [typeof(Grid)] = () => (IElementHandler)(object)new GridHandler(), - [typeof(StackLayout)] = () => (IElementHandler)(object)new StackLayoutHandler(), - [typeof(VerticalStackLayout)] = () => (IElementHandler)(object)new StackLayoutHandler(), - [typeof(HorizontalStackLayout)] = () => (IElementHandler)(object)new StackLayoutHandler(), - [typeof(AbsoluteLayout)] = () => (IElementHandler)(object)new LayoutHandler(), - [typeof(FlexLayout)] = () => (IElementHandler)(object)new LayoutHandler(), - [typeof(CollectionView)] = () => (IElementHandler)(object)new CollectionViewHandler(), - [typeof(ListView)] = () => (IElementHandler)(object)new CollectionViewHandler(), - [typeof(Page)] = () => (IElementHandler)(object)new PageHandler(), - [typeof(ContentPage)] = () => (IElementHandler)(object)new ContentPageHandler(), - [typeof(NavigationPage)] = () => (IElementHandler)(object)new NavigationPageHandler(), - [typeof(Shell)] = () => (IElementHandler)(object)new ShellHandler(), - [typeof(FlyoutPage)] = () => (IElementHandler)(object)new FlyoutPageHandler(), - [typeof(TabbedPage)] = () => (IElementHandler)(object)new TabbedPageHandler(), - [typeof(Application)] = () => (IElementHandler)(object)new ApplicationHandler(), - [typeof(Window)] = () => (IElementHandler)(object)new WindowHandler(), - [typeof(GraphicsView)] = () => (IElementHandler)(object)new GraphicsViewHandler() - }; - - public static IElementHandler ToHandler(this IElement element, IMauiContext mauiContext) - { - return CreateHandler(element, mauiContext); - } - - public static IViewHandler? ToViewHandler(this IView view, IMauiContext mauiContext) - { - IElementHandler? obj = CreateHandler((IElement)(object)view, mauiContext); - return (IViewHandler?)(object)((obj is IViewHandler) ? obj : null); - } - - private static IElementHandler? CreateHandler(IElement element, IMauiContext mauiContext) - { - Type type = ((object)element).GetType(); - IElementHandler val = null; - if (LinuxHandlerMap.TryGetValue(type, out Func value)) - { - val = value(); - Console.WriteLine("[ToHandler] Using Linux handler for " + type.Name + ": " + ((object)val).GetType().Name); - } - else - { - Type type2 = null; - Func func = null; - foreach (KeyValuePair> item in LinuxHandlerMap) - { - if (item.Key.IsAssignableFrom(type) && (type2 == null || type2.IsAssignableFrom(item.Key))) - { - type2 = item.Key; - func = item.Value; - } - } - if (func != null) - { - val = func(); - Console.WriteLine($"[ToHandler] Using Linux handler (via base {type2.Name}) for {type.Name}: {((object)val).GetType().Name}"); - } - } - if (val == null) - { - val = mauiContext.Handlers.GetHandler(type); - Console.WriteLine("[ToHandler] Using MAUI handler for " + type.Name + ": " + (((object)val)?.GetType().Name ?? "null")); - } - if (val != null) - { - val.SetMauiContext(mauiContext); - val.SetVirtualView(element); - } - return val; - } -} diff --git a/Hosting/ScopedLinuxMauiContext.cs b/Hosting/ScopedLinuxMauiContext.cs deleted file mode 100644 index 2875bd0..0000000 --- a/Hosting/ScopedLinuxMauiContext.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; - -namespace Microsoft.Maui.Platform.Linux.Hosting; - -public class ScopedLinuxMauiContext : IMauiContext -{ - private readonly LinuxMauiContext _parent; - - public IServiceProvider Services => _parent.Services; - - public IMauiHandlersFactory Handlers => _parent.Handlers; - - public ScopedLinuxMauiContext(LinuxMauiContext parent) - { - _parent = parent ?? throw new ArgumentNullException("parent"); - } -} diff --git a/Interop/ClientMessageData.cs b/Interop/ClientMessageData.cs deleted file mode 100644 index 3030e6e..0000000 --- a/Interop/ClientMessageData.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Runtime.InteropServices; - -namespace Microsoft.Maui.Platform.Linux.Interop; - -[StructLayout(LayoutKind.Explicit)] -public struct ClientMessageData -{ - [FieldOffset(0)] - public long L0; - - [FieldOffset(8)] - public long L1; - - [FieldOffset(16)] - public long L2; - - [FieldOffset(24)] - public long L3; - - [FieldOffset(32)] - public long L4; -} diff --git a/Interop/WebKitGtk.cs b/Interop/WebKitGtk.cs deleted file mode 100644 index 8af03d6..0000000 --- a/Interop/WebKitGtk.cs +++ /dev/null @@ -1,260 +0,0 @@ -using System; -using System.Runtime.InteropServices; - -namespace Microsoft.Maui.Platform.Linux.Interop; - -public static class WebKitGtk -{ - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - public delegate void GCallback(); - - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - public delegate void LoadChangedCallback(IntPtr webView, int loadEvent, IntPtr userData); - - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - public delegate bool DecidePolicyCallback(IntPtr webView, IntPtr decision, int decisionType, IntPtr userData); - - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - public delegate void LoadFailedCallback(IntPtr webView, int loadEvent, IntPtr failingUri, IntPtr error, IntPtr userData); - - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - public delegate void NotifyCallback(IntPtr webView, IntPtr paramSpec, IntPtr userData); - - private const string WebKit2Lib = "libwebkit2gtk-4.1.so.0"; - - private const string GtkLib = "libgtk-3.so.0"; - - private const string GObjectLib = "libgobject-2.0.so.0"; - - private const string GLibLib = "libglib-2.0.so.0"; - - public const int WEBKIT_COOKIE_POLICY_ACCEPT_ALWAYS = 0; - - public const int WEBKIT_COOKIE_POLICY_ACCEPT_NEVER = 1; - - public const int WEBKIT_COOKIE_POLICY_ACCEPT_NO_THIRD_PARTY = 2; - - public const int WEBKIT_COOKIE_PERSISTENT_STORAGE_TEXT = 0; - - public const int WEBKIT_COOKIE_PERSISTENT_STORAGE_SQLITE = 1; - - public const int WEBKIT_LOAD_STARTED = 0; - - public const int WEBKIT_LOAD_REDIRECTED = 1; - - public const int WEBKIT_LOAD_COMMITTED = 2; - - public const int WEBKIT_LOAD_FINISHED = 3; - - public const int WEBKIT_POLICY_DECISION_TYPE_NAVIGATION_ACTION = 0; - - public const int WEBKIT_POLICY_DECISION_TYPE_NEW_WINDOW_ACTION = 1; - - public const int WEBKIT_POLICY_DECISION_TYPE_RESPONSE = 2; - - [DllImport("libgtk-3.so.0", CallingConvention = CallingConvention.Cdecl)] - public static extern bool gtk_init_check(ref int argc, ref IntPtr argv); - - [DllImport("libgtk-3.so.0", CallingConvention = CallingConvention.Cdecl)] - public static extern void gtk_main(); - - [DllImport("libgtk-3.so.0", CallingConvention = CallingConvention.Cdecl)] - public static extern void gtk_main_quit(); - - [DllImport("libgtk-3.so.0", CallingConvention = CallingConvention.Cdecl)] - public static extern bool gtk_events_pending(); - - [DllImport("libgtk-3.so.0", CallingConvention = CallingConvention.Cdecl)] - public static extern void gtk_main_iteration(); - - [DllImport("libgtk-3.so.0", CallingConvention = CallingConvention.Cdecl)] - public static extern bool gtk_main_iteration_do(bool blocking); - - [DllImport("libgtk-3.so.0", CallingConvention = CallingConvention.Cdecl)] - public static extern IntPtr gtk_window_new(int type); - - [DllImport("libgtk-3.so.0", CallingConvention = CallingConvention.Cdecl)] - public static extern void gtk_window_set_default_size(IntPtr window, int width, int height); - - [DllImport("libgtk-3.so.0", CallingConvention = CallingConvention.Cdecl)] - public static extern void gtk_window_set_decorated(IntPtr window, bool decorated); - - [DllImport("libgtk-3.so.0", CallingConvention = CallingConvention.Cdecl)] - public static extern void gtk_window_move(IntPtr window, int x, int y); - - [DllImport("libgtk-3.so.0", CallingConvention = CallingConvention.Cdecl)] - public static extern void gtk_window_resize(IntPtr window, int width, int height); - - [DllImport("libgtk-3.so.0", CallingConvention = CallingConvention.Cdecl)] - public static extern void gtk_widget_show_all(IntPtr widget); - - [DllImport("libgtk-3.so.0", CallingConvention = CallingConvention.Cdecl)] - public static extern void gtk_widget_show(IntPtr widget); - - [DllImport("libgtk-3.so.0", CallingConvention = CallingConvention.Cdecl)] - public static extern void gtk_widget_hide(IntPtr widget); - - [DllImport("libgtk-3.so.0", CallingConvention = CallingConvention.Cdecl)] - public static extern void gtk_widget_destroy(IntPtr widget); - - [DllImport("libgtk-3.so.0", CallingConvention = CallingConvention.Cdecl)] - public static extern void gtk_widget_set_size_request(IntPtr widget, int width, int height); - - [DllImport("libgtk-3.so.0", CallingConvention = CallingConvention.Cdecl)] - public static extern void gtk_widget_realize(IntPtr widget); - - [DllImport("libgtk-3.so.0", CallingConvention = CallingConvention.Cdecl)] - public static extern IntPtr gtk_widget_get_window(IntPtr widget); - - [DllImport("libgtk-3.so.0", CallingConvention = CallingConvention.Cdecl)] - public static extern void gtk_widget_set_can_focus(IntPtr widget, bool canFocus); - - [DllImport("libgtk-3.so.0", CallingConvention = CallingConvention.Cdecl)] - public static extern void gtk_container_add(IntPtr container, IntPtr widget); - - [DllImport("libgtk-3.so.0", CallingConvention = CallingConvention.Cdecl)] - public static extern void gtk_container_remove(IntPtr container, IntPtr widget); - - [DllImport("libgtk-3.so.0", CallingConvention = CallingConvention.Cdecl)] - public static extern IntPtr gtk_plug_new(ulong socketId); - - [DllImport("libgtk-3.so.0", CallingConvention = CallingConvention.Cdecl)] - public static extern ulong gtk_plug_get_id(IntPtr plug); - - [DllImport("libwebkit2gtk-4.1.so.0", CallingConvention = CallingConvention.Cdecl)] - public static extern IntPtr webkit_web_view_new(); - - [DllImport("libwebkit2gtk-4.1.so.0", CallingConvention = CallingConvention.Cdecl)] - public static extern IntPtr webkit_web_view_new_with_context(IntPtr context); - - [DllImport("libwebkit2gtk-4.1.so.0", CallingConvention = CallingConvention.Cdecl)] - public static extern void webkit_web_view_load_uri(IntPtr webView, [MarshalAs(UnmanagedType.LPUTF8Str)] string uri); - - [DllImport("libwebkit2gtk-4.1.so.0", CallingConvention = CallingConvention.Cdecl)] - public static extern void webkit_web_view_load_html(IntPtr webView, [MarshalAs(UnmanagedType.LPUTF8Str)] string content, [MarshalAs(UnmanagedType.LPUTF8Str)] string? baseUri); - - [DllImport("libwebkit2gtk-4.1.so.0", CallingConvention = CallingConvention.Cdecl)] - public static extern void webkit_web_view_reload(IntPtr webView); - - [DllImport("libwebkit2gtk-4.1.so.0", CallingConvention = CallingConvention.Cdecl)] - public static extern void webkit_web_view_stop_loading(IntPtr webView); - - [DllImport("libwebkit2gtk-4.1.so.0", CallingConvention = CallingConvention.Cdecl)] - public static extern void webkit_web_view_go_back(IntPtr webView); - - [DllImport("libwebkit2gtk-4.1.so.0", CallingConvention = CallingConvention.Cdecl)] - public static extern void webkit_web_view_go_forward(IntPtr webView); - - [DllImport("libwebkit2gtk-4.1.so.0", CallingConvention = CallingConvention.Cdecl)] - public static extern bool webkit_web_view_can_go_back(IntPtr webView); - - [DllImport("libwebkit2gtk-4.1.so.0", CallingConvention = CallingConvention.Cdecl)] - public static extern bool webkit_web_view_can_go_forward(IntPtr webView); - - [DllImport("libwebkit2gtk-4.1.so.0", CallingConvention = CallingConvention.Cdecl)] - public static extern IntPtr webkit_web_view_get_uri(IntPtr webView); - - [DllImport("libwebkit2gtk-4.1.so.0", CallingConvention = CallingConvention.Cdecl)] - public static extern IntPtr webkit_web_view_get_title(IntPtr webView); - - [DllImport("libwebkit2gtk-4.1.so.0", CallingConvention = CallingConvention.Cdecl)] - public static extern double webkit_web_view_get_estimated_load_progress(IntPtr webView); - - [DllImport("libwebkit2gtk-4.1.so.0", CallingConvention = CallingConvention.Cdecl)] - public static extern bool webkit_web_view_is_loading(IntPtr webView); - - [DllImport("libwebkit2gtk-4.1.so.0", CallingConvention = CallingConvention.Cdecl)] - public static extern void webkit_web_view_run_javascript(IntPtr webView, [MarshalAs(UnmanagedType.LPUTF8Str)] string script, IntPtr cancellable, IntPtr callback, IntPtr userData); - - [DllImport("libwebkit2gtk-4.1.so.0", CallingConvention = CallingConvention.Cdecl)] - public static extern IntPtr webkit_web_view_run_javascript_finish(IntPtr webView, IntPtr result, out IntPtr error); - - [DllImport("libwebkit2gtk-4.1.so.0", CallingConvention = CallingConvention.Cdecl)] - public static extern IntPtr webkit_web_view_get_settings(IntPtr webView); - - [DllImport("libwebkit2gtk-4.1.so.0", CallingConvention = CallingConvention.Cdecl)] - public static extern void webkit_settings_set_enable_javascript(IntPtr settings, bool enabled); - - [DllImport("libwebkit2gtk-4.1.so.0", CallingConvention = CallingConvention.Cdecl)] - public static extern void webkit_settings_set_user_agent(IntPtr settings, [MarshalAs(UnmanagedType.LPUTF8Str)] string userAgent); - - [DllImport("libwebkit2gtk-4.1.so.0", CallingConvention = CallingConvention.Cdecl)] - public static extern IntPtr webkit_settings_get_user_agent(IntPtr settings); - - [DllImport("libwebkit2gtk-4.1.so.0", CallingConvention = CallingConvention.Cdecl)] - public static extern void webkit_settings_set_enable_developer_extras(IntPtr settings, bool enabled); - - [DllImport("libwebkit2gtk-4.1.so.0", CallingConvention = CallingConvention.Cdecl)] - public static extern void webkit_settings_set_javascript_can_access_clipboard(IntPtr settings, bool enabled); - - [DllImport("libwebkit2gtk-4.1.so.0", CallingConvention = CallingConvention.Cdecl)] - public static extern void webkit_settings_set_enable_webgl(IntPtr settings, bool enabled); - - [DllImport("libwebkit2gtk-4.1.so.0", CallingConvention = CallingConvention.Cdecl)] - public static extern void webkit_settings_set_allow_file_access_from_file_urls(IntPtr settings, bool enabled); - - [DllImport("libwebkit2gtk-4.1.so.0", CallingConvention = CallingConvention.Cdecl)] - public static extern void webkit_settings_set_allow_universal_access_from_file_urls(IntPtr settings, bool enabled); - - [DllImport("libwebkit2gtk-4.1.so.0", CallingConvention = CallingConvention.Cdecl)] - public static extern IntPtr webkit_web_context_get_default(); - - [DllImport("libwebkit2gtk-4.1.so.0", CallingConvention = CallingConvention.Cdecl)] - public static extern IntPtr webkit_web_context_new(); - - [DllImport("libwebkit2gtk-4.1.so.0", CallingConvention = CallingConvention.Cdecl)] - public static extern IntPtr webkit_web_context_get_cookie_manager(IntPtr context); - - [DllImport("libwebkit2gtk-4.1.so.0", CallingConvention = CallingConvention.Cdecl)] - public static extern void webkit_cookie_manager_set_accept_policy(IntPtr cookieManager, int policy); - - [DllImport("libwebkit2gtk-4.1.so.0", CallingConvention = CallingConvention.Cdecl)] - public static extern void webkit_cookie_manager_set_persistent_storage(IntPtr cookieManager, [MarshalAs(UnmanagedType.LPUTF8Str)] string filename, int storage); - - [DllImport("libwebkit2gtk-4.1.so.0", CallingConvention = CallingConvention.Cdecl)] - public static extern IntPtr webkit_navigation_action_get_request(IntPtr action); - - [DllImport("libwebkit2gtk-4.1.so.0", CallingConvention = CallingConvention.Cdecl)] - public static extern int webkit_navigation_action_get_navigation_type(IntPtr action); - - [DllImport("libwebkit2gtk-4.1.so.0", CallingConvention = CallingConvention.Cdecl)] - public static extern IntPtr webkit_uri_request_get_uri(IntPtr request); - - [DllImport("libwebkit2gtk-4.1.so.0", CallingConvention = CallingConvention.Cdecl)] - public static extern void webkit_policy_decision_use(IntPtr decision); - - [DllImport("libwebkit2gtk-4.1.so.0", CallingConvention = CallingConvention.Cdecl)] - public static extern void webkit_policy_decision_ignore(IntPtr decision); - - [DllImport("libwebkit2gtk-4.1.so.0", CallingConvention = CallingConvention.Cdecl)] - public static extern void webkit_policy_decision_download(IntPtr decision); - - [DllImport("libgobject-2.0.so.0", CallingConvention = CallingConvention.Cdecl)] - public static extern ulong g_signal_connect_data(IntPtr instance, [MarshalAs(UnmanagedType.LPUTF8Str)] string detailedSignal, Delegate handler, IntPtr data, IntPtr destroyData, int connectFlags); - - [DllImport("libgobject-2.0.so.0", CallingConvention = CallingConvention.Cdecl)] - public static extern void g_signal_handler_disconnect(IntPtr instance, ulong handlerId); - - [DllImport("libgobject-2.0.so.0", CallingConvention = CallingConvention.Cdecl)] - public static extern void g_object_unref(IntPtr obj); - - [DllImport("libglib-2.0.so.0", CallingConvention = CallingConvention.Cdecl)] - public static extern void g_free(IntPtr mem); - - public static string? PtrToStringUtf8(IntPtr ptr) - { - if (ptr == IntPtr.Zero) - { - return null; - } - return Marshal.PtrToStringUTF8(ptr); - } - - public static void ProcessGtkEvents() - { - while (gtk_events_pending()) - { - gtk_main_iteration_do(blocking: false); - } - } -} diff --git a/Interop/X11.cs b/Interop/X11.cs deleted file mode 100644 index 3832365..0000000 --- a/Interop/X11.cs +++ /dev/null @@ -1,442 +0,0 @@ -using System; -using System.CodeDom.Compiler; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Runtime.InteropServices.Marshalling; - -namespace Microsoft.Maui.Platform.Linux.Interop; - -internal static class X11 -{ - private const string LibX11 = "libX11.so.6"; - - private const string LibXext = "libXext.so.6"; - - public const int ZPixmap = 2; - - [DllImport("libX11.so.6", ExactSpelling = true)] - [LibraryImport("libX11.so.6")] - public static extern IntPtr XOpenDisplay(IntPtr displayName); - - [DllImport("libX11.so.6", ExactSpelling = true)] - [LibraryImport("libX11.so.6")] - public static extern int XCloseDisplay(IntPtr display); - - [DllImport("libX11.so.6", ExactSpelling = true)] - [LibraryImport("libX11.so.6")] - public static extern int XDefaultScreen(IntPtr display); - - [DllImport("libX11.so.6", ExactSpelling = true)] - [LibraryImport("libX11.so.6")] - public static extern IntPtr XRootWindow(IntPtr display, int screenNumber); - - [DllImport("libX11.so.6", ExactSpelling = true)] - [LibraryImport("libX11.so.6")] - public static extern int XDisplayWidth(IntPtr display, int screenNumber); - - [DllImport("libX11.so.6", ExactSpelling = true)] - [LibraryImport("libX11.so.6")] - public static extern int XDisplayHeight(IntPtr display, int screenNumber); - - [DllImport("libX11.so.6", ExactSpelling = true)] - [LibraryImport("libX11.so.6")] - public static extern int XDefaultDepth(IntPtr display, int screenNumber); - - [DllImport("libX11.so.6", ExactSpelling = true)] - [LibraryImport("libX11.so.6")] - public static extern IntPtr XDefaultVisual(IntPtr display, int screenNumber); - - [DllImport("libX11.so.6", ExactSpelling = true)] - [LibraryImport("libX11.so.6")] - public static extern IntPtr XDefaultColormap(IntPtr display, int screenNumber); - - [DllImport("libX11.so.6", ExactSpelling = true)] - [LibraryImport("libX11.so.6")] - public static extern int XFlush(IntPtr display); - - [LibraryImport("libX11.so.6")] - [GeneratedCode("Microsoft.Interop.LibraryImportGenerator", "9.0.11.2809")] - [SkipLocalsInit] - public static int XSync(IntPtr display, [MarshalAs(UnmanagedType.Bool)] bool discard) - { - int _discard_native = (discard ? 1 : 0); - return __PInvoke(display, _discard_native); - [DllImport("libX11.so.6", EntryPoint = "XSync", ExactSpelling = true)] - static extern int __PInvoke(IntPtr __display_native, int __discard_native); - } - - [DllImport("libX11.so.6", ExactSpelling = true)] - [LibraryImport("libX11.so.6")] - public static extern IntPtr XCreateSimpleWindow(IntPtr display, IntPtr parent, int x, int y, uint width, uint height, uint borderWidth, ulong border, ulong background); - - [LibraryImport("libX11.so.6")] - [GeneratedCode("Microsoft.Interop.LibraryImportGenerator", "9.0.11.2809")] - [SkipLocalsInit] - public unsafe static IntPtr XCreateWindow(IntPtr display, IntPtr parent, int x, int y, uint width, uint height, uint borderWidth, int depth, uint windowClass, IntPtr visual, ulong valueMask, ref XSetWindowAttributes attributes) - { - IntPtr result; - fixed (XSetWindowAttributes* _attributes_native = &attributes) - { - result = __PInvoke(display, parent, x, y, width, height, borderWidth, depth, windowClass, visual, valueMask, _attributes_native); - } - return result; - [DllImport("libX11.so.6", EntryPoint = "XCreateWindow", ExactSpelling = true)] - static extern unsafe IntPtr __PInvoke(IntPtr __display_native, IntPtr __parent_native, int __x_native, int __y_native, uint __width_native, uint __height_native, uint __borderWidth_native, int __depth_native, uint __windowClass_native, IntPtr __visual_native, ulong __valueMask_native, XSetWindowAttributes* __attributes_native); - } - - [DllImport("libX11.so.6", ExactSpelling = true)] - [LibraryImport("libX11.so.6")] - public static extern int XDestroyWindow(IntPtr display, IntPtr window); - - [DllImport("libX11.so.6", ExactSpelling = true)] - [LibraryImport("libX11.so.6")] - public static extern int XMapWindow(IntPtr display, IntPtr window); - - [DllImport("libX11.so.6", ExactSpelling = true)] - [LibraryImport("libX11.so.6")] - public static extern int XUnmapWindow(IntPtr display, IntPtr window); - - [DllImport("libX11.so.6", ExactSpelling = true)] - [LibraryImport("libX11.so.6")] - public static extern int XMoveWindow(IntPtr display, IntPtr window, int x, int y); - - [DllImport("libX11.so.6", ExactSpelling = true)] - [LibraryImport("libX11.so.6")] - public static extern int XResizeWindow(IntPtr display, IntPtr window, uint width, uint height); - - [DllImport("libX11.so.6", ExactSpelling = true)] - [LibraryImport("libX11.so.6")] - public static extern int XMoveResizeWindow(IntPtr display, IntPtr window, int x, int y, uint width, uint height); - - [LibraryImport("libX11.so.6", StringMarshalling = StringMarshalling.Utf8)] - [GeneratedCode("Microsoft.Interop.LibraryImportGenerator", "9.0.11.2809")] - [SkipLocalsInit] - public unsafe static int XStoreName(IntPtr display, IntPtr window, string windowName) - { - byte* ptr = default(byte*); - int num = 0; - Utf8StringMarshaller.ManagedToUnmanagedIn managedToUnmanagedIn = default(Utf8StringMarshaller.ManagedToUnmanagedIn); - try - { - Span buffer = stackalloc byte[Utf8StringMarshaller.ManagedToUnmanagedIn.BufferSize]; - managedToUnmanagedIn.FromManaged(windowName, buffer); - ptr = managedToUnmanagedIn.ToUnmanaged(); - return __PInvoke(display, window, ptr); - } - finally - { - managedToUnmanagedIn.Free(); - } - [DllImport("libX11.so.6", EntryPoint = "XStoreName", ExactSpelling = true)] - static extern unsafe int __PInvoke(IntPtr __display_native, IntPtr __window_native, byte* __windowName_native); - } - - [DllImport("libX11.so.6", ExactSpelling = true)] - [LibraryImport("libX11.so.6")] - public static extern int XRaiseWindow(IntPtr display, IntPtr window); - - [DllImport("libX11.so.6", ExactSpelling = true)] - [LibraryImport("libX11.so.6")] - public static extern int XLowerWindow(IntPtr display, IntPtr window); - - [DllImport("libX11.so.6", ExactSpelling = true)] - [LibraryImport("libX11.so.6")] - public static extern int XSelectInput(IntPtr display, IntPtr window, long eventMask); - - [LibraryImport("libX11.so.6")] - [GeneratedCode("Microsoft.Interop.LibraryImportGenerator", "9.0.11.2809")] - [SkipLocalsInit] - public unsafe static int XNextEvent(IntPtr display, out XEvent eventReturn) - { - eventReturn = default(XEvent); - int result; - fixed (XEvent* _eventReturn_native = &eventReturn) - { - result = __PInvoke(display, _eventReturn_native); - } - return result; - [DllImport("libX11.so.6", EntryPoint = "XNextEvent", ExactSpelling = true)] - static extern unsafe int __PInvoke(IntPtr __display_native, XEvent* __eventReturn_native); - } - - [LibraryImport("libX11.so.6")] - [GeneratedCode("Microsoft.Interop.LibraryImportGenerator", "9.0.11.2809")] - [SkipLocalsInit] - public unsafe static int XPeekEvent(IntPtr display, out XEvent eventReturn) - { - eventReturn = default(XEvent); - int result; - fixed (XEvent* _eventReturn_native = &eventReturn) - { - result = __PInvoke(display, _eventReturn_native); - } - return result; - [DllImport("libX11.so.6", EntryPoint = "XPeekEvent", ExactSpelling = true)] - static extern unsafe int __PInvoke(IntPtr __display_native, XEvent* __eventReturn_native); - } - - [DllImport("libX11.so.6", ExactSpelling = true)] - [LibraryImport("libX11.so.6")] - public static extern int XPending(IntPtr display); - - [LibraryImport("libX11.so.6")] - [GeneratedCode("Microsoft.Interop.LibraryImportGenerator", "9.0.11.2809")] - [SkipLocalsInit] - [return: MarshalAs(UnmanagedType.Bool)] - public unsafe static bool XCheckTypedWindowEvent(IntPtr display, IntPtr window, int eventType, out XEvent eventReturn) - { - eventReturn = default(XEvent); - int num; - fixed (XEvent* _eventReturn_native = &eventReturn) - { - num = __PInvoke(display, window, eventType, _eventReturn_native); - } - return num != 0; - [DllImport("libX11.so.6", EntryPoint = "XCheckTypedWindowEvent", ExactSpelling = true)] - static extern unsafe int __PInvoke(IntPtr __display_native, IntPtr __window_native, int __eventType_native, XEvent* __eventReturn_native); - } - - [LibraryImport("libX11.so.6")] - [GeneratedCode("Microsoft.Interop.LibraryImportGenerator", "9.0.11.2809")] - [SkipLocalsInit] - public unsafe static int XSendEvent(IntPtr display, IntPtr window, [MarshalAs(UnmanagedType.Bool)] bool propagate, long eventMask, ref XEvent eventSend) - { - int _propagate_native = (propagate ? 1 : 0); - int result; - fixed (XEvent* _eventSend_native = &eventSend) - { - result = __PInvoke(display, window, _propagate_native, eventMask, _eventSend_native); - } - return result; - [DllImport("libX11.so.6", EntryPoint = "XSendEvent", ExactSpelling = true)] - static extern unsafe int __PInvoke(IntPtr __display_native, IntPtr __window_native, int __propagate_native, long __eventMask_native, XEvent* __eventSend_native); - } - - [DllImport("libX11.so.6", ExactSpelling = true)] - [LibraryImport("libX11.so.6")] - public static extern ulong XKeycodeToKeysym(IntPtr display, int keycode, int index); - - [LibraryImport("libX11.so.6")] - [GeneratedCode("Microsoft.Interop.LibraryImportGenerator", "9.0.11.2809")] - [SkipLocalsInit] - public unsafe static int XLookupString(ref XKeyEvent keyEvent, IntPtr bufferReturn, int bytesBuffer, out ulong keysymReturn, IntPtr statusInOut) - { - keysymReturn = 0uL; - int result; - fixed (ulong* _keysymReturn_native = &keysymReturn) - { - fixed (XKeyEvent* _keyEvent_native = &keyEvent) - { - result = __PInvoke(_keyEvent_native, bufferReturn, bytesBuffer, _keysymReturn_native, statusInOut); - } - } - return result; - [DllImport("libX11.so.6", EntryPoint = "XLookupString", ExactSpelling = true)] - static extern unsafe int __PInvoke(XKeyEvent* __keyEvent_native, IntPtr __bufferReturn_native, int __bytesBuffer_native, ulong* __keysymReturn_native, IntPtr __statusInOut_native); - } - - [LibraryImport("libX11.so.6")] - [GeneratedCode("Microsoft.Interop.LibraryImportGenerator", "9.0.11.2809")] - [SkipLocalsInit] - public static int XGrabKeyboard(IntPtr display, IntPtr grabWindow, [MarshalAs(UnmanagedType.Bool)] bool ownerEvents, int pointerMode, int keyboardMode, ulong time) - { - int _ownerEvents_native = (ownerEvents ? 1 : 0); - return __PInvoke(display, grabWindow, _ownerEvents_native, pointerMode, keyboardMode, time); - [DllImport("libX11.so.6", EntryPoint = "XGrabKeyboard", ExactSpelling = true)] - static extern int __PInvoke(IntPtr __display_native, IntPtr __grabWindow_native, int __ownerEvents_native, int __pointerMode_native, int __keyboardMode_native, ulong __time_native); - } - - [DllImport("libX11.so.6", ExactSpelling = true)] - [LibraryImport("libX11.so.6")] - public static extern int XUngrabKeyboard(IntPtr display, ulong time); - - [LibraryImport("libX11.so.6")] - [GeneratedCode("Microsoft.Interop.LibraryImportGenerator", "9.0.11.2809")] - [SkipLocalsInit] - public static int XGrabPointer(IntPtr display, IntPtr grabWindow, [MarshalAs(UnmanagedType.Bool)] bool ownerEvents, uint eventMask, int pointerMode, int keyboardMode, IntPtr confineTo, IntPtr cursor, ulong time) - { - int _ownerEvents_native = (ownerEvents ? 1 : 0); - return __PInvoke(display, grabWindow, _ownerEvents_native, eventMask, pointerMode, keyboardMode, confineTo, cursor, time); - [DllImport("libX11.so.6", EntryPoint = "XGrabPointer", ExactSpelling = true)] - static extern int __PInvoke(IntPtr __display_native, IntPtr __grabWindow_native, int __ownerEvents_native, uint __eventMask_native, int __pointerMode_native, int __keyboardMode_native, IntPtr __confineTo_native, IntPtr __cursor_native, ulong __time_native); - } - - [DllImport("libX11.so.6", ExactSpelling = true)] - [LibraryImport("libX11.so.6")] - public static extern int XUngrabPointer(IntPtr display, ulong time); - - [LibraryImport("libX11.so.6")] - [GeneratedCode("Microsoft.Interop.LibraryImportGenerator", "9.0.11.2809")] - [SkipLocalsInit] - [return: MarshalAs(UnmanagedType.Bool)] - public unsafe static bool XQueryPointer(IntPtr display, IntPtr window, out IntPtr rootReturn, out IntPtr childReturn, out int rootX, out int rootY, out int winX, out int winY, out uint maskReturn) - { - rootReturn = (IntPtr)0; - childReturn = (IntPtr)0; - rootX = 0; - rootY = 0; - winX = 0; - winY = 0; - maskReturn = 0u; - int num; - fixed (uint* _maskReturn_native = &maskReturn) - { - fixed (int* _winY_native = &winY) - { - fixed (int* _winX_native = &winX) - { - fixed (int* _rootY_native = &rootY) - { - fixed (int* _rootX_native = &rootX) - { - fixed (IntPtr* _childReturn_native = &childReturn) - { - fixed (IntPtr* _rootReturn_native = &rootReturn) - { - num = __PInvoke(display, window, _rootReturn_native, _childReturn_native, _rootX_native, _rootY_native, _winX_native, _winY_native, _maskReturn_native); - } - } - } - } - } - } - } - return num != 0; - [DllImport("libX11.so.6", EntryPoint = "XQueryPointer", ExactSpelling = true)] - static extern unsafe int __PInvoke(IntPtr __display_native, IntPtr __window_native, IntPtr* __rootReturn_native, IntPtr* __childReturn_native, int* __rootX_native, int* __rootY_native, int* __winX_native, int* __winY_native, uint* __maskReturn_native); - } - - [DllImport("libX11.so.6", ExactSpelling = true)] - [LibraryImport("libX11.so.6")] - public static extern int XWarpPointer(IntPtr display, IntPtr srcWindow, IntPtr destWindow, int srcX, int srcY, uint srcWidth, uint srcHeight, int destX, int destY); - - [LibraryImport("libX11.so.6", StringMarshalling = StringMarshalling.Utf8)] - [GeneratedCode("Microsoft.Interop.LibraryImportGenerator", "9.0.11.2809")] - [SkipLocalsInit] - public unsafe static IntPtr XInternAtom(IntPtr display, string atomName, [MarshalAs(UnmanagedType.Bool)] bool onlyIfExists) - { - byte* ptr = default(byte*); - int num = 0; - nint num2 = 0; - Utf8StringMarshaller.ManagedToUnmanagedIn managedToUnmanagedIn = default(Utf8StringMarshaller.ManagedToUnmanagedIn); - try - { - num = (onlyIfExists ? 1 : 0); - Span buffer = stackalloc byte[Utf8StringMarshaller.ManagedToUnmanagedIn.BufferSize]; - managedToUnmanagedIn.FromManaged(atomName, buffer); - ptr = managedToUnmanagedIn.ToUnmanaged(); - return __PInvoke(display, ptr, num); - } - finally - { - managedToUnmanagedIn.Free(); - } - [DllImport("libX11.so.6", EntryPoint = "XInternAtom", ExactSpelling = true)] - static extern unsafe IntPtr __PInvoke(IntPtr __display_native, byte* __atomName_native, int __onlyIfExists_native); - } - - [DllImport("libX11.so.6", ExactSpelling = true)] - [LibraryImport("libX11.so.6")] - public static extern int XChangeProperty(IntPtr display, IntPtr window, IntPtr property, IntPtr type, int format, int mode, IntPtr data, int nelements); - - [LibraryImport("libX11.so.6")] - [GeneratedCode("Microsoft.Interop.LibraryImportGenerator", "9.0.11.2809")] - [SkipLocalsInit] - public unsafe static int XGetWindowProperty(IntPtr display, IntPtr window, IntPtr property, long longOffset, long longLength, [MarshalAs(UnmanagedType.Bool)] bool delete, IntPtr reqType, out IntPtr actualTypeReturn, out int actualFormatReturn, out IntPtr nitemsReturn, out IntPtr bytesAfterReturn, out IntPtr propReturn) - { - actualTypeReturn = (IntPtr)0; - actualFormatReturn = 0; - nitemsReturn = (IntPtr)0; - bytesAfterReturn = (IntPtr)0; - propReturn = (IntPtr)0; - int _delete_native = (delete ? 1 : 0); - int result; - fixed (IntPtr* _propReturn_native = &propReturn) - { - fixed (IntPtr* _bytesAfterReturn_native = &bytesAfterReturn) - { - fixed (IntPtr* _nitemsReturn_native = &nitemsReturn) - { - fixed (int* _actualFormatReturn_native = &actualFormatReturn) - { - fixed (IntPtr* _actualTypeReturn_native = &actualTypeReturn) - { - result = __PInvoke(display, window, property, longOffset, longLength, _delete_native, reqType, _actualTypeReturn_native, _actualFormatReturn_native, _nitemsReturn_native, _bytesAfterReturn_native, _propReturn_native); - } - } - } - } - } - return result; - [DllImport("libX11.so.6", EntryPoint = "XGetWindowProperty", ExactSpelling = true)] - static extern unsafe int __PInvoke(IntPtr __display_native, IntPtr __window_native, IntPtr __property_native, long __longOffset_native, long __longLength_native, int __delete_native, IntPtr __reqType_native, IntPtr* __actualTypeReturn_native, int* __actualFormatReturn_native, IntPtr* __nitemsReturn_native, IntPtr* __bytesAfterReturn_native, IntPtr* __propReturn_native); - } - - [DllImport("libX11.so.6", ExactSpelling = true)] - [LibraryImport("libX11.so.6")] - public static extern int XDeleteProperty(IntPtr display, IntPtr window, IntPtr property); - - [DllImport("libX11.so.6", ExactSpelling = true)] - [LibraryImport("libX11.so.6")] - public static extern int XSetSelectionOwner(IntPtr display, IntPtr selection, IntPtr owner, ulong time); - - [DllImport("libX11.so.6", ExactSpelling = true)] - [LibraryImport("libX11.so.6")] - public static extern IntPtr XGetSelectionOwner(IntPtr display, IntPtr selection); - - [DllImport("libX11.so.6", ExactSpelling = true)] - [LibraryImport("libX11.so.6")] - public static extern int XConvertSelection(IntPtr display, IntPtr selection, IntPtr target, IntPtr property, IntPtr requestor, ulong time); - - [DllImport("libX11.so.6", ExactSpelling = true)] - [LibraryImport("libX11.so.6")] - public static extern int XFree(IntPtr data); - - [DllImport("libX11.so.6", ExactSpelling = true)] - [LibraryImport("libX11.so.6")] - public static extern IntPtr XCreateGC(IntPtr display, IntPtr drawable, ulong valueMask, IntPtr values); - - [DllImport("libX11.so.6", ExactSpelling = true)] - [LibraryImport("libX11.so.6")] - public static extern int XFreeGC(IntPtr display, IntPtr gc); - - [DllImport("libX11.so.6", ExactSpelling = true)] - [LibraryImport("libX11.so.6")] - public static extern int XCopyArea(IntPtr display, IntPtr src, IntPtr dest, IntPtr gc, int srcX, int srcY, uint width, uint height, int destX, int destY); - - [DllImport("libX11.so.6", ExactSpelling = true)] - [LibraryImport("libX11.so.6")] - public static extern IntPtr XCreateFontCursor(IntPtr display, uint shape); - - [DllImport("libX11.so.6", ExactSpelling = true)] - [LibraryImport("libX11.so.6")] - public static extern int XFreeCursor(IntPtr display, IntPtr cursor); - - [DllImport("libX11.so.6", ExactSpelling = true)] - [LibraryImport("libX11.so.6")] - public static extern int XDefineCursor(IntPtr display, IntPtr window, IntPtr cursor); - - [DllImport("libX11.so.6", ExactSpelling = true)] - [LibraryImport("libX11.so.6")] - public static extern int XUndefineCursor(IntPtr display, IntPtr window); - - [DllImport("libX11.so.6", ExactSpelling = true)] - [LibraryImport("libX11.so.6")] - public static extern int XConnectionNumber(IntPtr display); - - [DllImport("libX11.so.6", ExactSpelling = true)] - [LibraryImport("libX11.so.6")] - public static extern IntPtr XCreateImage(IntPtr display, IntPtr visual, uint depth, int format, int offset, IntPtr data, uint width, uint height, int bitmapPad, int bytesPerLine); - - [DllImport("libX11.so.6", ExactSpelling = true)] - [LibraryImport("libX11.so.6")] - public static extern int XPutImage(IntPtr display, IntPtr drawable, IntPtr gc, IntPtr image, int srcX, int srcY, int destX, int destY, uint width, uint height); - - [DllImport("libX11.so.6", ExactSpelling = true)] - [LibraryImport("libX11.so.6")] - public static extern int XDestroyImage(IntPtr image); - - [DllImport("libX11.so.6", ExactSpelling = true)] - [LibraryImport("libX11.so.6")] - public static extern IntPtr XDefaultGC(IntPtr display, int screen); -} diff --git a/Interop/XButtonEvent.cs b/Interop/XButtonEvent.cs deleted file mode 100644 index 68a619c..0000000 --- a/Interop/XButtonEvent.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; - -namespace Microsoft.Maui.Platform.Linux.Interop; - -public struct XButtonEvent -{ - public int Type; - - public ulong Serial; - - public int SendEvent; - - public IntPtr Display; - - public IntPtr Window; - - public IntPtr Root; - - public IntPtr Subwindow; - - public ulong Time; - - public int X; - - public int Y; - - public int XRoot; - - public int YRoot; - - public uint State; - - public uint Button; - - public int SameScreen; -} diff --git a/Interop/XClientMessageEvent.cs b/Interop/XClientMessageEvent.cs deleted file mode 100644 index 5ae9f81..0000000 --- a/Interop/XClientMessageEvent.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; - -namespace Microsoft.Maui.Platform.Linux.Interop; - -public struct XClientMessageEvent -{ - public int Type; - - public ulong Serial; - - public int SendEvent; - - public IntPtr Display; - - public IntPtr Window; - - public IntPtr MessageType; - - public int Format; - - public ClientMessageData Data; -} diff --git a/Interop/XConfigureEvent.cs b/Interop/XConfigureEvent.cs deleted file mode 100644 index ec80e7c..0000000 --- a/Interop/XConfigureEvent.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; - -namespace Microsoft.Maui.Platform.Linux.Interop; - -public struct XConfigureEvent -{ - public int Type; - - public ulong Serial; - - public int SendEvent; - - public IntPtr Display; - - public IntPtr Event; - - public IntPtr Window; - - public int X; - - public int Y; - - public int Width; - - public int Height; - - public int BorderWidth; - - public IntPtr Above; - - public int OverrideRedirect; -} diff --git a/Interop/XCrossingEvent.cs b/Interop/XCrossingEvent.cs deleted file mode 100644 index 8c80cd1..0000000 --- a/Interop/XCrossingEvent.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; - -namespace Microsoft.Maui.Platform.Linux.Interop; - -public struct XCrossingEvent -{ - public int Type; - - public ulong Serial; - - public int SendEvent; - - public IntPtr Display; - - public IntPtr Window; - - public IntPtr Root; - - public IntPtr Subwindow; - - public ulong Time; - - public int X; - - public int Y; - - public int XRoot; - - public int YRoot; - - public int Mode; - - public int Detail; - - public int SameScreen; - - public int Focus; - - public uint State; -} diff --git a/Interop/XCursorShape.cs b/Interop/XCursorShape.cs deleted file mode 100644 index c44a596..0000000 --- a/Interop/XCursorShape.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Microsoft.Maui.Platform.Linux.Interop; - -public static class XCursorShape -{ - public const uint XC_left_ptr = 68u; - - public const uint XC_hand2 = 60u; - - public const uint XC_xterm = 152u; - - public const uint XC_watch = 150u; - - public const uint XC_crosshair = 34u; -} diff --git a/Interop/XEvent.cs b/Interop/XEvent.cs deleted file mode 100644 index f1654a2..0000000 --- a/Interop/XEvent.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System.Runtime.InteropServices; - -namespace Microsoft.Maui.Platform.Linux.Interop; - -[StructLayout(LayoutKind.Explicit, Size = 192)] -public struct XEvent -{ - [FieldOffset(0)] - public int Type; - - [FieldOffset(0)] - public XKeyEvent KeyEvent; - - [FieldOffset(0)] - public XButtonEvent ButtonEvent; - - [FieldOffset(0)] - public XMotionEvent MotionEvent; - - [FieldOffset(0)] - public XConfigureEvent ConfigureEvent; - - [FieldOffset(0)] - public XExposeEvent ExposeEvent; - - [FieldOffset(0)] - public XClientMessageEvent ClientMessageEvent; - - [FieldOffset(0)] - public XCrossingEvent CrossingEvent; - - [FieldOffset(0)] - public XFocusChangeEvent FocusChangeEvent; -} diff --git a/Interop/XEventMask.cs b/Interop/XEventMask.cs deleted file mode 100644 index 8587dc0..0000000 --- a/Interop/XEventMask.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace Microsoft.Maui.Platform.Linux.Interop; - -public static class XEventMask -{ - public const long KeyPressMask = 1L; - - public const long KeyReleaseMask = 2L; - - public const long ButtonPressMask = 4L; - - public const long ButtonReleaseMask = 8L; - - public const long EnterWindowMask = 16L; - - public const long LeaveWindowMask = 32L; - - public const long PointerMotionMask = 64L; - - public const long ExposureMask = 32768L; - - public const long StructureNotifyMask = 131072L; - - public const long FocusChangeMask = 2097152L; -} diff --git a/Interop/XEventType.cs b/Interop/XEventType.cs deleted file mode 100644 index 73c7af7..0000000 --- a/Interop/XEventType.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace Microsoft.Maui.Platform.Linux.Interop; - -public static class XEventType -{ - public const int KeyPress = 2; - - public const int KeyRelease = 3; - - public const int ButtonPress = 4; - - public const int ButtonRelease = 5; - - public const int MotionNotify = 6; - - public const int EnterNotify = 7; - - public const int LeaveNotify = 8; - - public const int FocusIn = 9; - - public const int FocusOut = 10; - - public const int Expose = 12; - - public const int ConfigureNotify = 22; - - public const int ClientMessage = 33; -} diff --git a/Interop/XExposeEvent.cs b/Interop/XExposeEvent.cs deleted file mode 100644 index f7ab485..0000000 --- a/Interop/XExposeEvent.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; - -namespace Microsoft.Maui.Platform.Linux.Interop; - -public struct XExposeEvent -{ - public int Type; - - public ulong Serial; - - public int SendEvent; - - public IntPtr Display; - - public IntPtr Window; - - public int X; - - public int Y; - - public int Width; - - public int Height; - - public int Count; -} diff --git a/Interop/XFocusChangeEvent.cs b/Interop/XFocusChangeEvent.cs deleted file mode 100644 index 27ca4fa..0000000 --- a/Interop/XFocusChangeEvent.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; - -namespace Microsoft.Maui.Platform.Linux.Interop; - -public struct XFocusChangeEvent -{ - public int Type; - - public ulong Serial; - - public int SendEvent; - - public IntPtr Display; - - public IntPtr Window; - - public int Mode; - - public int Detail; -} diff --git a/Interop/XKeyEvent.cs b/Interop/XKeyEvent.cs deleted file mode 100644 index 9cc73e3..0000000 --- a/Interop/XKeyEvent.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; - -namespace Microsoft.Maui.Platform.Linux.Interop; - -public struct XKeyEvent -{ - public int Type; - - public ulong Serial; - - public int SendEvent; - - public IntPtr Display; - - public IntPtr Window; - - public IntPtr Root; - - public IntPtr Subwindow; - - public ulong Time; - - public int X; - - public int Y; - - public int XRoot; - - public int YRoot; - - public uint State; - - public uint Keycode; - - public int SameScreen; -} diff --git a/Interop/XMotionEvent.cs b/Interop/XMotionEvent.cs deleted file mode 100644 index 4fdf4cc..0000000 --- a/Interop/XMotionEvent.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; - -namespace Microsoft.Maui.Platform.Linux.Interop; - -public struct XMotionEvent -{ - public int Type; - - public ulong Serial; - - public int SendEvent; - - public IntPtr Display; - - public IntPtr Window; - - public IntPtr Root; - - public IntPtr Subwindow; - - public ulong Time; - - public int X; - - public int Y; - - public int XRoot; - - public int YRoot; - - public uint State; - - public byte IsHint; - - public int SameScreen; -} diff --git a/Interop/XSetWindowAttributes.cs b/Interop/XSetWindowAttributes.cs deleted file mode 100644 index af66b05..0000000 --- a/Interop/XSetWindowAttributes.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; - -namespace Microsoft.Maui.Platform.Linux.Interop; - -public struct XSetWindowAttributes -{ - public IntPtr BackgroundPixmap; - - public ulong BackgroundPixel; - - public IntPtr BorderPixmap; - - public ulong BorderPixel; - - public int BitGravity; - - public int WinGravity; - - public int BackingStore; - - public ulong BackingPlanes; - - public ulong BackingPixel; - - public int SaveUnder; - - public long EventMask; - - public long DoNotPropagateMask; - - public int OverrideRedirect; - - public IntPtr Colormap; - - public IntPtr Cursor; -} diff --git a/Interop/XWindowClass.cs b/Interop/XWindowClass.cs deleted file mode 100644 index ca922c4..0000000 --- a/Interop/XWindowClass.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Microsoft.Maui.Platform.Linux.Interop; - -public static class XWindowClass -{ - public const uint InputOutput = 1u; - - public const uint InputOnly = 2u; -} diff --git a/LinuxApplication.cs b/LinuxApplication.cs index 27fb6be..fbb0fc6 100644 --- a/LinuxApplication.cs +++ b/LinuxApplication.cs @@ -1,1019 +1,562 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.IO; -using System.Reflection; -using System.Threading; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using Microsoft.Extensions.DependencyInjection; -using Microsoft.Maui.Controls; -using Microsoft.Maui.Dispatching; using Microsoft.Maui.Hosting; -using Microsoft.Maui.Platform.Linux.Dispatching; -using Microsoft.Maui.Platform.Linux.Hosting; -using Microsoft.Maui.Platform.Linux.Native; using Microsoft.Maui.Platform.Linux.Rendering; -using Microsoft.Maui.Platform.Linux.Services; using Microsoft.Maui.Platform.Linux.Window; -using SkiaSharp; +using Microsoft.Maui.Platform.Linux.Services; +using Microsoft.Maui.Platform; namespace Microsoft.Maui.Platform.Linux; +/// +/// Main Linux application class that bootstraps the MAUI application. +/// public class LinuxApplication : IDisposable { - private static int _invalidateCount; + private X11Window? _mainWindow; + private SkiaRenderingEngine? _renderingEngine; + private SkiaView? _rootView; + private SkiaView? _focusedView; + private SkiaView? _hoveredView; + private SkiaView? _capturedView; // View that has captured pointer events during drag + private bool _disposed; - private static int _requestRedrawCount; + /// + /// Gets the current application instance. + /// + public static LinuxApplication? Current { get; private set; } - private static int _drawCount; + /// + /// Gets the main window. + /// + public X11Window? MainWindow => _mainWindow; - private static int _gtkThreadId; + /// + /// Gets the rendering engine. + /// + public SkiaRenderingEngine? RenderingEngine => _renderingEngine; - private static DateTime _lastCounterReset = DateTime.Now; + /// + /// Gets or sets the root view. + /// + public SkiaView? RootView + { + get => _rootView; + set + { + _rootView = value; + if (_rootView != null && _mainWindow != null) + { + _rootView.Arrange(new SkiaSharp.SKRect( + 0, 0, + _mainWindow.Width, + _mainWindow.Height)); + } + } + } - private X11Window? _mainWindow; + /// + /// Gets or sets the currently focused view. + /// + public SkiaView? FocusedView + { + get => _focusedView; + set + { + if (_focusedView != value) + { + if (_focusedView != null) + { + _focusedView.IsFocused = false; + } - private GtkHostWindow? _gtkWindow; + _focusedView = value; - private SkiaRenderingEngine? _renderingEngine; + if (_focusedView != null) + { + _focusedView.IsFocused = true; + } + } + } + } - private SkiaView? _rootView; + /// + /// Creates a new Linux application. + /// + public LinuxApplication() + { + Current = this; - private SkiaView? _focusedView; + // Set up dialog service invalidation callback + LinuxDialogService.SetInvalidateCallback(() => _renderingEngine?.InvalidateAll()); + } - private SkiaView? _hoveredView; + /// + /// Runs a MAUI application on Linux. + /// This is the main entry point for Linux apps. + /// + /// The MauiApp to run. + /// Command line arguments. + public static void Run(MauiApp app, string[] args) + { + Run(app, args, null); + } - private SkiaView? _capturedView; + /// + /// Runs a MAUI application on Linux with options. + /// + /// The MauiApp to run. + /// Command line arguments. + /// Optional configuration action. + public static void Run(MauiApp app, string[] args, Action? configure) + { + var options = app.Services.GetService() + ?? new LinuxApplicationOptions(); + configure?.Invoke(options); + ParseCommandLineOptions(args, options); - private bool _disposed; + using var linuxApp = new LinuxApplication(); + linuxApp.Initialize(options); - private bool _useGtk; + // Create MAUI context + var mauiContext = new Hosting.LinuxMauiContext(app.Services, linuxApp); - private static bool _isRedrawing; + // Get the application and render it + var application = app.Services.GetService(); + SkiaView? rootView = null; - private static int _loopCounter = 0; + if (application is Microsoft.Maui.Controls.Application mauiApplication) + { + // Force Application.Current to be this instance + // The constructor sets Current = this, but we ensure it here + var currentProperty = typeof(Microsoft.Maui.Controls.Application).GetProperty("Current"); + if (currentProperty != null && currentProperty.CanWrite) + { + currentProperty.SetValue(null, mauiApplication); + } - public static LinuxApplication? Current { get; private set; } + if (mauiApplication.MainPage != null) + { + // Create a MAUI Window and add it to the application + // This ensures Shell.Current works (it reads from Application.Current.Windows[0].Page) + var mainPage = mauiApplication.MainPage; - public static bool IsGtkMode => Current?._useGtk ?? false; + // Always ensure we have a window with the Shell/Page + var windowsField = typeof(Microsoft.Maui.Controls.Application).GetField("_windows", + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + var windowsList = windowsField?.GetValue(mauiApplication) as System.Collections.Generic.List; - public X11Window? MainWindow => _mainWindow; + if (windowsList != null && windowsList.Count == 0) + { + var mauiWindow = new Microsoft.Maui.Controls.Window(mainPage); + windowsList.Add(mauiWindow); + mauiWindow.Parent = mauiApplication; + } + else if (windowsList != null && windowsList.Count > 0 && windowsList[0].Page == null) + { + // Window exists but has no page - set it + windowsList[0].Page = mainPage; + } - public SkiaRenderingEngine? RenderingEngine => _renderingEngine; + var renderer = new Hosting.LinuxViewRenderer(mauiContext); + rootView = renderer.RenderPage(mainPage); - public SkiaView? RootView - { - get - { - return _rootView; - } - set - { - //IL_003f: Unknown result type (might be due to invalid IL or missing references) - _rootView = value; - if (_rootView != null && _mainWindow != null) - { - _rootView.Arrange(new SKRect(0f, 0f, (float)_mainWindow.Width, (float)_mainWindow.Height)); - } - } - } + // Update window title based on app name (NavigationPage.Title takes precedence) + string windowTitle = "OpenMaui App"; + if (mainPage is Microsoft.Maui.Controls.NavigationPage navPage) + { + // Prefer NavigationPage.Title (app name) over CurrentPage.Title (page name) for window title + windowTitle = navPage.Title ?? windowTitle; + } + else if (mainPage is Microsoft.Maui.Controls.Shell shell) + { + windowTitle = shell.Title ?? windowTitle; + } + else + { + windowTitle = mainPage.Title ?? windowTitle; + } + linuxApp.SetWindowTitle(windowTitle); + } + } - public SkiaView? FocusedView - { - get - { - return _focusedView; - } - set - { - if (_focusedView != value) - { - if (_focusedView != null) - { - _focusedView.IsFocused = false; - } - _focusedView = value; - if (_focusedView != null) - { - _focusedView.IsFocused = true; - } - } - } - } + // Fallback to demo if no view + if (rootView == null) + { + rootView = Hosting.LinuxProgramHost.CreateDemoView(); + } - public static void LogInvalidate(string source) - { - int currentManagedThreadId = Environment.CurrentManagedThreadId; - Interlocked.Increment(ref _invalidateCount); - if (currentManagedThreadId != _gtkThreadId && _gtkThreadId != 0) - { - Console.WriteLine($"[DIAG] ⚠\ufe0f Invalidate from WRONG THREAD! GTK={_gtkThreadId}, Current={currentManagedThreadId}, Source={source}"); - } - } + linuxApp.RootView = rootView; + linuxApp.Run(); + } - public static void LogRequestRedraw() - { - int currentManagedThreadId = Environment.CurrentManagedThreadId; - Interlocked.Increment(ref _requestRedrawCount); - if (currentManagedThreadId != _gtkThreadId && _gtkThreadId != 0) - { - Console.WriteLine($"[DIAG] ⚠\ufe0f RequestRedraw from WRONG THREAD! GTK={_gtkThreadId}, Current={currentManagedThreadId}"); - } - } + private static void ParseCommandLineOptions(string[] args, LinuxApplicationOptions options) + { + for (int i = 0; i < args.Length; i++) + { + switch (args[i].ToLowerInvariant()) + { + case "--title" when i + 1 < args.Length: + options.Title = args[++i]; + break; + case "--width" when i + 1 < args.Length && int.TryParse(args[i + 1], out var w): + options.Width = w; + i++; + break; + case "--height" when i + 1 < args.Length && int.TryParse(args[i + 1], out var h): + options.Height = h; + i++; + break; + } + } + } - public static void LogDraw() - { - Interlocked.Increment(ref _drawCount); - } + /// + /// Initializes the application with the specified options. + /// + public void Initialize(LinuxApplicationOptions options) + { + // Create the main window + _mainWindow = new X11Window( + options.Title ?? "MAUI Application", + options.Width, + options.Height); - private static void StartHeartbeat() - { - _gtkThreadId = Environment.CurrentManagedThreadId; - Console.WriteLine($"[DIAG] GTK thread ID: {_gtkThreadId}"); - GLibNative.TimeoutAdd(250u, delegate - { - DateTime now = DateTime.Now; - if ((now - _lastCounterReset).TotalSeconds >= 1.0) - { - int value = Interlocked.Exchange(ref _invalidateCount, 0); - int value2 = Interlocked.Exchange(ref _requestRedrawCount, 0); - int value3 = Interlocked.Exchange(ref _drawCount, 0); - Console.WriteLine($"[DIAG] ❤\ufe0f Heartbeat | Invalidate={value}/s, RequestRedraw={value2}/s, Draw={value3}/s"); - _lastCounterReset = now; - } - return true; - }); - } + // Create the rendering engine + _renderingEngine = new SkiaRenderingEngine(_mainWindow); - public LinuxApplication() - { - Current = this; - LinuxDialogService.SetInvalidateCallback(delegate - { - _renderingEngine?.InvalidateAll(); - }); - } + // Wire up events + _mainWindow.Resized += OnWindowResized; + _mainWindow.Exposed += OnWindowExposed; + _mainWindow.KeyDown += OnKeyDown; + _mainWindow.KeyUp += OnKeyUp; + _mainWindow.TextInput += OnTextInput; + _mainWindow.PointerMoved += OnPointerMoved; + _mainWindow.PointerPressed += OnPointerPressed; + _mainWindow.PointerReleased += OnPointerReleased; + _mainWindow.Scroll += OnScroll; + _mainWindow.CloseRequested += OnCloseRequested; - public static void Run(MauiApp app, string[] args) - { - Run(app, args, null); - } + // Register platform services + RegisterServices(); + } - public static void Run(MauiApp app, string[] args, Action? configure) - { - //IL_013e: Unknown result type (might be due to invalid IL or missing references) - //IL_0145: Expected O, but got Unknown - Microsoft.Maui.Platform.Linux.Dispatching.LinuxDispatcher.Initialize(); - DispatcherProvider.SetCurrent((IDispatcherProvider)(object)LinuxDispatcherProvider.Instance); - Console.WriteLine("[LinuxApplication] Dispatcher initialized"); - LinuxApplicationOptions linuxApplicationOptions = app.Services.GetService() ?? new LinuxApplicationOptions(); - configure?.Invoke(linuxApplicationOptions); - ParseCommandLineOptions(args, linuxApplicationOptions); - LinuxApplication linuxApp = new LinuxApplication(); - try - { - linuxApp.Initialize(linuxApplicationOptions); - LinuxMauiContext mauiContext = new LinuxMauiContext(app.Services, linuxApp); - IApplication service = app.Services.GetService(); - SkiaView skiaView = null; - Application mauiApplication = (Application)(object)((service is Application) ? service : null); - if (mauiApplication != null) - { - PropertyInfo property = typeof(Application).GetProperty("Current"); - if (property != null && property.CanWrite) - { - property.SetValue(null, mauiApplication); - } - ((BindableObject)mauiApplication).PropertyChanged += delegate(object? s, PropertyChangedEventArgs e) - { - //IL_0030: Unknown result type (might be due to invalid IL or missing references) - if (e.PropertyName == "UserAppTheme") - { - Console.WriteLine($"[LinuxApplication] Theme changed to: {mauiApplication.UserAppTheme}"); - LinuxViewRenderer.CurrentSkiaShell?.RefreshTheme(); - linuxApp._renderingEngine?.InvalidateAll(); - } - }; - if (mauiApplication.MainPage != null) - { - Page mainPage = mauiApplication.MainPage; - List list = typeof(Application).GetField("_windows", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(mauiApplication) as List; - if (list != null && list.Count == 0) - { - Window val = new Window(mainPage); - list.Add(val); - ((Element)val).Parent = (Element)(object)mauiApplication; - } - else if (list != null && list.Count > 0 && list[0].Page == null) - { - list[0].Page = mainPage; - } - skiaView = new LinuxViewRenderer((IMauiContext)(object)mauiContext).RenderPage(mainPage); - string text = "OpenMaui App"; - NavigationPage val2 = (NavigationPage)(object)((mainPage is NavigationPage) ? mainPage : null); - if (val2 != null) - { - text = ((Page)val2).Title ?? text; - } - else - { - Shell val3 = (Shell)(object)((mainPage is Shell) ? mainPage : null); - text = ((val3 == null) ? (mainPage.Title ?? text) : (((Page)val3).Title ?? text)); - } - linuxApp.SetWindowTitle(text); - } - } - if (skiaView == null) - { - skiaView = LinuxProgramHost.CreateDemoView(); - } - linuxApp.RootView = skiaView; - linuxApp.Run(); - } - finally - { - if (linuxApp != null) - { - ((IDisposable)linuxApp).Dispose(); - } - } - } + private void RegisterServices() + { + // Platform services would be registered with the DI container here + // For now, we create singleton instances + } - private static void ParseCommandLineOptions(string[] args, LinuxApplicationOptions options) - { - for (int i = 0; i < args.Length; i++) - { - switch (args[i].ToLowerInvariant()) - { - case "--title": - if (i + 1 < args.Length) - { - options.Title = args[++i]; - } - break; - case "--width": - { - if (i + 1 < args.Length && int.TryParse(args[i + 1], out var result2)) - { - options.Width = result2; - i++; - } - break; - } - case "--height": - { - if (i + 1 < args.Length && int.TryParse(args[i + 1], out var result)) - { - options.Height = result; - i++; - } - break; - } - } - } - } + /// + /// Sets the window title. + /// + public void SetWindowTitle(string title) + { + _mainWindow?.SetTitle(title); + } - public void Initialize(LinuxApplicationOptions options) - { - _useGtk = options.UseGtk; - if (_useGtk) - { - InitializeGtk(options); - } - else - { - InitializeX11(options); - } - RegisterServices(); - } + /// + /// Shows the main window and runs the event loop. + /// + public void Run() + { + if (_mainWindow == null) + throw new InvalidOperationException("Application not initialized"); - private void InitializeX11(LinuxApplicationOptions options) - { - _mainWindow = new X11Window(options.Title ?? "MAUI Application", options.Width, options.Height); - SkiaWebView.SetMainWindow(_mainWindow.Display, _mainWindow.Handle); - string text = ResolveIconPath(options.IconPath); - if (!string.IsNullOrEmpty(text)) - { - _mainWindow.SetIcon(text); - } - _renderingEngine = new SkiaRenderingEngine(_mainWindow); - _mainWindow.Resized += OnWindowResized; - _mainWindow.Exposed += OnWindowExposed; - _mainWindow.KeyDown += OnKeyDown; - _mainWindow.KeyUp += OnKeyUp; - _mainWindow.TextInput += OnTextInput; - _mainWindow.PointerMoved += OnPointerMoved; - _mainWindow.PointerPressed += OnPointerPressed; - _mainWindow.PointerReleased += OnPointerReleased; - _mainWindow.Scroll += OnScroll; - _mainWindow.CloseRequested += OnCloseRequested; - } + _mainWindow.Show(); - private void InitializeGtk(LinuxApplicationOptions options) - { - _gtkWindow = GtkHostService.Instance.GetOrCreateHostWindow(options.Title ?? "MAUI Application", options.Width, options.Height); - string text = ResolveIconPath(options.IconPath); - if (!string.IsNullOrEmpty(text)) - { - GtkHostService.Instance.SetWindowIcon(text); - } - if (_gtkWindow.SkiaSurface != null) - { - _gtkWindow.SkiaSurface.DrawRequested += OnGtkDrawRequested; - _gtkWindow.SkiaSurface.PointerPressed += OnGtkPointerPressed; - _gtkWindow.SkiaSurface.PointerReleased += OnGtkPointerReleased; - _gtkWindow.SkiaSurface.PointerMoved += OnGtkPointerMoved; - _gtkWindow.SkiaSurface.KeyPressed += OnGtkKeyPressed; - _gtkWindow.SkiaSurface.KeyReleased += OnGtkKeyReleased; - _gtkWindow.SkiaSurface.Scrolled += OnGtkScrolled; - _gtkWindow.SkiaSurface.TextInput += OnGtkTextInput; - } - _gtkWindow.Resized += OnGtkResized; - } + // Initial render + Render(); - private void RegisterServices() - { - } + // Run the event loop + while (_mainWindow.IsRunning) + { + _mainWindow.ProcessEvents(); - private static string? ResolveIconPath(string? explicitPath) - { - if (!string.IsNullOrEmpty(explicitPath)) - { - if (Path.IsPathRooted(explicitPath)) - { - if (!File.Exists(explicitPath)) - { - return null; - } - return explicitPath; - } - string text = Path.Combine(AppContext.BaseDirectory, explicitPath); - if (!File.Exists(text)) - { - return null; - } - return text; - } - string baseDirectory = AppContext.BaseDirectory; - string text2 = Path.Combine(baseDirectory, "appicon.meta"); - if (File.Exists(text2)) - { - string text3 = MauiIconGenerator.GenerateIcon(text2); - if (!string.IsNullOrEmpty(text3) && File.Exists(text3)) - { - return text3; - } - } - string text4 = Path.Combine(baseDirectory, "appicon.png"); - if (File.Exists(text4)) - { - return text4; - } - string text5 = Path.Combine(baseDirectory, "appicon.svg"); - if (File.Exists(text5)) - { - return text5; - } - return null; - } + // Update animations and render + UpdateAnimations(); + Render(); - public void SetWindowTitle(string title) - { - _mainWindow?.SetTitle(title); - } + // Small delay to prevent 100% CPU usage + Thread.Sleep(1); + } + } - public static void RequestRedraw() - { - LogRequestRedraw(); - if (_isRedrawing) - { - return; - } - _isRedrawing = true; - try - { - LinuxApplication? current = Current; - if (current != null && current._useGtk) - { - Current._gtkWindow?.RequestRedraw(); - } - else - { - Current?._renderingEngine?.InvalidateAll(); - } - } - finally - { - _isRedrawing = false; - } - } + private void UpdateAnimations() + { + // Update cursor blink for entry controls + if (_focusedView is SkiaEntry entry) + { + entry.UpdateCursorBlink(); + } + } - public void Run() - { - if (_useGtk) - { - RunGtk(); - } - else - { - RunX11(); - } - } + private void Render() + { + if (_renderingEngine != null && _rootView != null) + { + _renderingEngine.Render(_rootView); + } + } - private void RunX11() - { - if (_mainWindow == null) - { - throw new InvalidOperationException("Application not initialized"); - } - _mainWindow.Show(); - Render(); - Console.WriteLine("[LinuxApplication] Starting event loop"); - while (_mainWindow.IsRunning) - { - _loopCounter++; - if (_loopCounter % 1000 == 0) - { - Console.WriteLine($"[LinuxApplication] Loop iteration {_loopCounter}"); - } - _mainWindow.ProcessEvents(); - SkiaWebView.ProcessGtkEvents(); - UpdateAnimations(); - Render(); - Thread.Sleep(1); - } - Console.WriteLine("[LinuxApplication] Event loop ended"); - } + private void OnWindowResized(object? sender, (int Width, int Height) size) + { + if (_rootView != null) + { + // Re-measure with new available size, then arrange + var availableSize = new SkiaSharp.SKSize(size.Width, size.Height); + _rootView.Measure(availableSize); + _rootView.Arrange(new SkiaSharp.SKRect(0, 0, size.Width, size.Height)); + } + _renderingEngine?.InvalidateAll(); + } - private void RunGtk() - { - if (_gtkWindow == null) - { - throw new InvalidOperationException("Application not initialized"); - } - StartHeartbeat(); - PerformGtkLayout(_gtkWindow.Width, _gtkWindow.Height); - _gtkWindow.RequestRedraw(); - _gtkWindow.Run(); - GtkHostService.Instance.Shutdown(); - } + private void OnWindowExposed(object? sender, EventArgs e) + { + Render(); + } - private void PerformGtkLayout(int width, int height) - { - //IL_0013: Unknown result type (might be due to invalid IL or missing references) - //IL_0018: Unknown result type (might be due to invalid IL or missing references) - //IL_0032: Unknown result type (might be due to invalid IL or missing references) - if (_rootView != null) - { - _rootView.Measure(new SKSize((float)width, (float)height)); - _rootView.Arrange(new SKRect(0f, 0f, (float)width, (float)height)); - } - } + private void OnKeyDown(object? sender, KeyEventArgs e) + { + // Route to dialog if one is active + if (LinuxDialogService.HasActiveDialog) + { + LinuxDialogService.TopDialog?.OnKeyDown(e); + return; + } - private void UpdateAnimations() - { - if (_focusedView is SkiaEntry skiaEntry) - { - skiaEntry.UpdateCursorBlink(); - } - } + if (_focusedView != null) + { + _focusedView.OnKeyDown(e); + } + } - private void Render() - { - if (_renderingEngine != null && _rootView != null) - { - _renderingEngine.Render(_rootView); - } - } + private void OnKeyUp(object? sender, KeyEventArgs e) + { + // Route to dialog if one is active + if (LinuxDialogService.HasActiveDialog) + { + LinuxDialogService.TopDialog?.OnKeyUp(e); + return; + } - private void OnWindowResized(object? sender, (int Width, int Height) size) - { - //IL_0023: Unknown result type (might be due to invalid IL or missing references) - //IL_0024: Unknown result type (might be due to invalid IL or missing references) - //IL_0048: Unknown result type (might be due to invalid IL or missing references) - if (_rootView != null) - { - SKSize availableSize = default(SKSize); - ((SKSize)(ref availableSize))._002Ector((float)size.Width, (float)size.Height); - _rootView.Measure(availableSize); - _rootView.Arrange(new SKRect(0f, 0f, (float)size.Width, (float)size.Height)); - } - _renderingEngine?.InvalidateAll(); - } + if (_focusedView != null) + { + _focusedView.OnKeyUp(e); + } + } - private void OnWindowExposed(object? sender, EventArgs e) - { - Render(); - } + private void OnTextInput(object? sender, TextInputEventArgs e) + { + if (_focusedView != null) + { + _focusedView.OnTextInput(e); + } + } - private void OnKeyDown(object? sender, KeyEventArgs e) - { - if (LinuxDialogService.HasActiveDialog) - { - LinuxDialogService.TopDialog?.OnKeyDown(e); - } - else if (_focusedView != null) - { - _focusedView.OnKeyDown(e); - } - } + private void OnPointerMoved(object? sender, PointerEventArgs e) + { + // Route to dialog if one is active + if (LinuxDialogService.HasActiveDialog) + { + LinuxDialogService.TopDialog?.OnPointerMoved(e); + return; + } - private void OnKeyUp(object? sender, KeyEventArgs e) - { - if (LinuxDialogService.HasActiveDialog) - { - LinuxDialogService.TopDialog?.OnKeyUp(e); - } - else if (_focusedView != null) - { - _focusedView.OnKeyUp(e); - } - } + if (_rootView != null) + { + // If a view has captured the pointer, send all events to it + if (_capturedView != null) + { + _capturedView.OnPointerMoved(e); + return; + } - private void OnTextInput(object? sender, TextInputEventArgs e) - { - if (_focusedView != null) - { - _focusedView.OnTextInput(e); - } - } + // Check for popup overlay first + var popupOwner = SkiaView.GetPopupOwnerAt(e.X, e.Y); + var hitView = popupOwner ?? _rootView.HitTest(e.X, e.Y); - private void OnPointerMoved(object? sender, PointerEventArgs e) - { - if (LinuxDialogService.HasContextMenu) - { - LinuxDialogService.ActiveContextMenu?.OnPointerMoved(e); - } - else if (LinuxDialogService.HasActiveDialog) - { - LinuxDialogService.TopDialog?.OnPointerMoved(e); - } - else - { - if (_rootView == null) - { - return; - } - if (_capturedView != null) - { - _capturedView.OnPointerMoved(e); - return; - } - SkiaView skiaView = SkiaView.GetPopupOwnerAt(e.X, e.Y) ?? _rootView.HitTest(e.X, e.Y); - if (skiaView != _hoveredView) - { - _hoveredView?.OnPointerExited(e); - _hoveredView = skiaView; - _hoveredView?.OnPointerEntered(e); - CursorType cursor = skiaView?.CursorType ?? CursorType.Arrow; - _mainWindow?.SetCursor(cursor); - } - skiaView?.OnPointerMoved(e); - } - } + // Track hover state changes + if (hitView != _hoveredView) + { + _hoveredView?.OnPointerExited(e); + _hoveredView = hitView; + _hoveredView?.OnPointerEntered(e); + } - private void OnPointerPressed(object? sender, PointerEventArgs e) - { - Console.WriteLine($"[LinuxApplication] OnPointerPressed at ({e.X}, {e.Y})"); - if (LinuxDialogService.HasContextMenu) - { - LinuxDialogService.ActiveContextMenu?.OnPointerPressed(e); - } - else if (LinuxDialogService.HasActiveDialog) - { - LinuxDialogService.TopDialog?.OnPointerPressed(e); - } - else - { - if (_rootView == null) - { - return; - } - SkiaView skiaView = SkiaView.GetPopupOwnerAt(e.X, e.Y) ?? _rootView.HitTest(e.X, e.Y); - Console.WriteLine("[LinuxApplication] HitView: " + (((object)skiaView)?.GetType().Name ?? "null") + ", rootView: " + ((object)_rootView).GetType().Name); - if (skiaView != null) - { - _capturedView = skiaView; - if (skiaView.IsFocusable) - { - FocusedView = skiaView; - } - Console.WriteLine("[LinuxApplication] Calling OnPointerPressed on " + ((object)skiaView).GetType().Name); - skiaView.OnPointerPressed(e); - } - else - { - if (SkiaView.HasActivePopup && _focusedView != null) - { - _focusedView.OnFocusLost(); - } - FocusedView = null; - } - } - } + hitView?.OnPointerMoved(e); + } + } - private void OnPointerReleased(object? sender, PointerEventArgs e) - { - Console.WriteLine($"[LinuxApplication] OnPointerReleased at ({e.X}, {e.Y}), capturedView={((object)_capturedView)?.GetType().Name ?? "null"}"); - if (LinuxDialogService.HasActiveDialog) - { - LinuxDialogService.TopDialog?.OnPointerReleased(e); - } - else if (_rootView != null) - { - if (_capturedView != null) - { - _capturedView.OnPointerReleased(e); - _capturedView = null; - } - else - { - (SkiaView.GetPopupOwnerAt(e.X, e.Y) ?? _rootView.HitTest(e.X, e.Y))?.OnPointerReleased(e); - } - } - } + private void OnPointerPressed(object? sender, PointerEventArgs e) + { + Console.WriteLine($"[LinuxApplication] OnPointerPressed at ({e.X}, {e.Y})"); - private void OnScroll(object? sender, ScrollEventArgs e) - { - Console.WriteLine($"[LinuxApplication] OnScroll - X={e.X}, Y={e.Y}, DeltaX={e.DeltaX}, DeltaY={e.DeltaY}"); - if (_rootView == null) - { - return; - } - SkiaView skiaView = _rootView.HitTest(e.X, e.Y); - Console.WriteLine("[LinuxApplication] HitView: " + (((object)skiaView)?.GetType().Name ?? "null")); - for (SkiaView skiaView2 = skiaView; skiaView2 != null; skiaView2 = skiaView2.Parent) - { - Console.WriteLine("[LinuxApplication] Bubbling to: " + ((object)skiaView2).GetType().Name); - if (skiaView2 is SkiaScrollView skiaScrollView) - { - skiaScrollView.OnScroll(e); - break; - } - skiaView2.OnScroll(e); - if (e.Handled) - { - break; - } - } - } + // Route to dialog if one is active + if (LinuxDialogService.HasActiveDialog) + { + LinuxDialogService.TopDialog?.OnPointerPressed(e); + return; + } - private void OnCloseRequested(object? sender, EventArgs e) - { - _mainWindow?.Stop(); - } + if (_rootView != null) + { + // Check for popup overlay first + var popupOwner = SkiaView.GetPopupOwnerAt(e.X, e.Y); + var hitView = popupOwner ?? _rootView.HitTest(e.X, e.Y); + Console.WriteLine($"[LinuxApplication] HitView: {hitView?.GetType().Name ?? "null"}, rootView: {_rootView.GetType().Name}"); - private void OnGtkDrawRequested(object? sender, EventArgs e) - { - //IL_0045: Unknown result type (might be due to invalid IL or missing references) - //IL_004b: Invalid comparison between Unknown and I4 - //IL_005c: Unknown result type (might be due to invalid IL or missing references) - //IL_004f: Unknown result type (might be due to invalid IL or missing references) - //IL_0061: Unknown result type (might be due to invalid IL or missing references) - //IL_0068: Unknown result type (might be due to invalid IL or missing references) - //IL_00b8: Unknown result type (might be due to invalid IL or missing references) - Console.WriteLine("[DIAG] >>> OnGtkDrawRequested ENTER"); - LogDraw(); - GtkSkiaSurfaceWidget gtkSkiaSurfaceWidget = _gtkWindow?.SkiaSurface; - if (gtkSkiaSurfaceWidget?.Canvas != null && _rootView != null) - { - Application current = Application.Current; - SKColor val = (SKColor)((current != null && (int)current.UserAppTheme == 2) ? new SKColor((byte)32, (byte)33, (byte)36) : SKColors.White); - gtkSkiaSurfaceWidget.Canvas.Clear(val); - Console.WriteLine("[DIAG] Drawing rootView..."); - _rootView.Draw(gtkSkiaSurfaceWidget.Canvas); - Console.WriteLine("[DIAG] Drawing dialogs..."); - SKRect bounds = default(SKRect); - ((SKRect)(ref bounds))._002Ector(0f, 0f, (float)gtkSkiaSurfaceWidget.Width, (float)gtkSkiaSurfaceWidget.Height); - LinuxDialogService.DrawDialogs(gtkSkiaSurfaceWidget.Canvas, bounds); - Console.WriteLine("[DIAG] <<< OnGtkDrawRequested EXIT"); - } - } + if (hitView != null) + { + // Capture pointer to this view for drag operations + _capturedView = hitView; - private void OnGtkResized(object? sender, (int Width, int Height) size) - { - PerformGtkLayout(size.Width, size.Height); - _gtkWindow?.RequestRedraw(); - } + // Update focus + if (hitView.IsFocusable) + { + FocusedView = hitView; + } - private void OnGtkPointerPressed(object? sender, (double X, double Y, int Button) e) - { - string value = ((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} ({value})"); - if (LinuxDialogService.HasContextMenu) - { - PointerButton button = ((e.Button == 1) ? PointerButton.Left : ((e.Button == 2) ? PointerButton.Middle : PointerButton.Right)); - PointerEventArgs e2 = new PointerEventArgs((float)e.X, (float)e.Y, button); - LinuxDialogService.ActiveContextMenu?.OnPointerPressed(e2); - _gtkWindow?.RequestRedraw(); - return; - } - if (_rootView == null) - { - Console.WriteLine("[LinuxApplication.GTK] _rootView is null!"); - return; - } - SkiaView skiaView = _rootView.HitTest((float)e.X, (float)e.Y); - Console.WriteLine("[LinuxApplication.GTK] HitView: " + (((object)skiaView)?.GetType().Name ?? "null")); - if (skiaView != null) - { - if (skiaView.IsFocusable && _focusedView != skiaView) - { - _focusedView?.OnFocusLost(); - _focusedView = skiaView; - _focusedView.OnFocusGained(); - } - _capturedView = skiaView; - PointerButton button2 = ((e.Button == 1) ? PointerButton.Left : ((e.Button == 2) ? PointerButton.Middle : PointerButton.Right)); - PointerEventArgs e3 = new PointerEventArgs((float)e.X, (float)e.Y, button2); - Console.WriteLine("[DIAG] >>> Before OnPointerPressed"); - skiaView.OnPointerPressed(e3); - Console.WriteLine("[DIAG] <<< After OnPointerPressed, calling RequestRedraw"); - _gtkWindow?.RequestRedraw(); - Console.WriteLine("[DIAG] <<< After RequestRedraw, returning from handler"); - } - } + Console.WriteLine($"[LinuxApplication] Calling OnPointerPressed on {hitView.GetType().Name}"); + hitView.OnPointerPressed(e); + } + else + { + // Close any open popups when clicking outside + if (SkiaView.HasActivePopup && _focusedView != null) + { + _focusedView.OnFocusLost(); + } + FocusedView = null; + } + } + } - private void OnGtkPointerReleased(object? sender, (double X, double Y, int Button) e) - { - Console.WriteLine("[DIAG] >>> OnGtkPointerReleased ENTER"); - if (_rootView == null) - { - return; - } - if (_capturedView != null) - { - PointerButton button = ((e.Button == 1) ? PointerButton.Left : ((e.Button == 2) ? PointerButton.Middle : PointerButton.Right)); - PointerEventArgs e2 = new PointerEventArgs((float)e.X, (float)e.Y, button); - Console.WriteLine("[DIAG] Calling OnPointerReleased on " + ((object)_capturedView).GetType().Name); - _capturedView.OnPointerReleased(e2); - Console.WriteLine("[DIAG] OnPointerReleased returned"); - _capturedView = null; - _gtkWindow?.RequestRedraw(); - Console.WriteLine("[DIAG] <<< OnGtkPointerReleased EXIT (captured path)"); - } - else - { - SkiaView skiaView = _rootView.HitTest((float)e.X, (float)e.Y); - if (skiaView != null) - { - PointerButton button2 = ((e.Button == 1) ? PointerButton.Left : ((e.Button == 2) ? PointerButton.Middle : PointerButton.Right)); - PointerEventArgs e3 = new PointerEventArgs((float)e.X, (float)e.Y, button2); - skiaView.OnPointerReleased(e3); - _gtkWindow?.RequestRedraw(); - } - } - } + private void OnPointerReleased(object? sender, PointerEventArgs e) + { + // Route to dialog if one is active + if (LinuxDialogService.HasActiveDialog) + { + LinuxDialogService.TopDialog?.OnPointerReleased(e); + return; + } - private void OnGtkPointerMoved(object? sender, (double X, double Y) e) - { - if (LinuxDialogService.HasContextMenu) - { - PointerEventArgs e2 = new PointerEventArgs((float)e.X, (float)e.Y); - LinuxDialogService.ActiveContextMenu?.OnPointerMoved(e2); - _gtkWindow?.RequestRedraw(); - } - else - { - if (_rootView == null) - { - return; - } - if (_capturedView != null) - { - PointerEventArgs e3 = new PointerEventArgs((float)e.X, (float)e.Y); - _capturedView.OnPointerMoved(e3); - _gtkWindow?.RequestRedraw(); - return; - } - SkiaView skiaView = _rootView.HitTest((float)e.X, (float)e.Y); - if (skiaView != _hoveredView) - { - PointerEventArgs e4 = new PointerEventArgs((float)e.X, (float)e.Y); - _hoveredView?.OnPointerExited(e4); - _hoveredView = skiaView; - _hoveredView?.OnPointerEntered(e4); - _gtkWindow?.RequestRedraw(); - } - if (skiaView != null) - { - PointerEventArgs e5 = new PointerEventArgs((float)e.X, (float)e.Y); - skiaView.OnPointerMoved(e5); - } - } - } + if (_rootView != null) + { + // If a view has captured the pointer, send release to it + if (_capturedView != null) + { + _capturedView.OnPointerReleased(e); + _capturedView = null; // Release capture + return; + } - private void OnGtkKeyPressed(object? sender, (uint KeyVal, uint KeyCode, uint State) e) - { - if (_focusedView != null) - { - Key key = ConvertGdkKey(e.KeyVal); - KeyModifiers modifiers = ConvertGdkModifiers(e.State); - KeyEventArgs e2 = new KeyEventArgs(key, modifiers); - _focusedView.OnKeyDown(e2); - _gtkWindow?.RequestRedraw(); - } - } + // Check for popup overlay first + var popupOwner = SkiaView.GetPopupOwnerAt(e.X, e.Y); + var hitView = popupOwner ?? _rootView.HitTest(e.X, e.Y); + hitView?.OnPointerReleased(e); + } + } - private void OnGtkKeyReleased(object? sender, (uint KeyVal, uint KeyCode, uint State) e) - { - if (_focusedView != null) - { - Key key = ConvertGdkKey(e.KeyVal); - KeyModifiers modifiers = ConvertGdkModifiers(e.State); - KeyEventArgs e2 = new KeyEventArgs(key, modifiers); - _focusedView.OnKeyUp(e2); - _gtkWindow?.RequestRedraw(); - } - } + private void OnScroll(object? sender, ScrollEventArgs e) + { + Console.WriteLine($"[LinuxApplication] OnScroll - X={e.X}, Y={e.Y}, DeltaX={e.DeltaX}, DeltaY={e.DeltaY}"); + if (_rootView != null) + { + var hitView = _rootView.HitTest(e.X, e.Y); + Console.WriteLine($"[LinuxApplication] HitView: {hitView?.GetType().Name ?? "null"}"); + // Bubble scroll events up to find a ScrollView + var view = hitView; + while (view != null) + { + Console.WriteLine($"[LinuxApplication] Bubbling to: {view.GetType().Name}"); + if (view is SkiaScrollView scrollView) + { + scrollView.OnScroll(e); + return; + } + view.OnScroll(e); + if (e.Handled) return; + view = view.Parent; + } + } + } - private void OnGtkScrolled(object? sender, (double X, double Y, double DeltaX, double DeltaY) e) - { - if (_rootView == null) - { - return; - } - for (SkiaView skiaView = _rootView.HitTest((float)e.X, (float)e.Y); skiaView != null; skiaView = skiaView.Parent) - { - if (skiaView is SkiaScrollView skiaScrollView) - { - ScrollEventArgs e2 = new ScrollEventArgs((float)e.X, (float)e.Y, (float)e.DeltaX, (float)e.DeltaY); - skiaScrollView.OnScroll(e2); - _gtkWindow?.RequestRedraw(); - break; - } - } - } + private void OnCloseRequested(object? sender, EventArgs e) + { + _mainWindow?.Stop(); + } - private void OnGtkTextInput(object? sender, string text) - { - if (_focusedView != null) - { - TextInputEventArgs e = new TextInputEventArgs(text); - _focusedView.OnTextInput(e); - _gtkWindow?.RequestRedraw(); - } - } + public void Dispose() + { + if (!_disposed) + { + _renderingEngine?.Dispose(); + _mainWindow?.Dispose(); - private static Key ConvertGdkKey(uint keyval) - { - switch (keyval) - { - case 65288u: - return Key.Backspace; - case 65289u: - return Key.Tab; - case 65293u: - return Key.Enter; - case 65307u: - return Key.Escape; - case 65360u: - return Key.Home; - case 65361u: - return Key.Left; - case 65362u: - return Key.Up; - case 65363u: - return Key.Right; - case 65364u: - return Key.Down; - case 65365u: - return Key.PageUp; - case 65366u: - return Key.PageDown; - case 65367u: - return Key.End; - case 65535u: - return Key.Delete; - case 32u: - case 33u: - case 34u: - case 35u: - case 36u: - case 37u: - case 38u: - case 39u: - case 40u: - case 41u: - case 42u: - case 43u: - case 44u: - case 45u: - case 46u: - case 47u: - case 48u: - case 49u: - case 50u: - case 51u: - case 52u: - case 53u: - case 54u: - case 55u: - case 56u: - case 57u: - case 58u: - case 59u: - case 60u: - case 61u: - case 62u: - case 63u: - case 64u: - case 65u: - case 66u: - case 67u: - case 68u: - case 69u: - case 70u: - case 71u: - case 72u: - case 73u: - case 74u: - case 75u: - case 76u: - case 77u: - case 78u: - case 79u: - case 80u: - case 81u: - case 82u: - case 83u: - case 84u: - case 85u: - case 86u: - case 87u: - case 88u: - case 89u: - case 90u: - case 91u: - case 92u: - case 93u: - case 94u: - case 95u: - case 96u: - case 97u: - case 98u: - case 99u: - case 100u: - case 101u: - case 102u: - case 103u: - case 104u: - case 105u: - case 106u: - case 107u: - case 108u: - case 109u: - case 110u: - case 111u: - case 112u: - case 113u: - case 114u: - case 115u: - case 116u: - case 117u: - case 118u: - case 119u: - case 120u: - case 121u: - case 122u: - case 123u: - case 124u: - case 125u: - case 126u: - return (Key)keyval; - default: - return Key.Unknown; - } - } + if (Current == this) + Current = null; - private static KeyModifiers ConvertGdkModifiers(uint state) - { - KeyModifiers keyModifiers = KeyModifiers.None; - if ((state & 1) != 0) - { - keyModifiers |= KeyModifiers.Shift; - } - if ((state & 4) != 0) - { - keyModifiers |= KeyModifiers.Control; - } - if ((state & 8) != 0) - { - keyModifiers |= KeyModifiers.Alt; - } - return keyModifiers; - } - - public void Dispose() - { - if (!_disposed) - { - _renderingEngine?.Dispose(); - _mainWindow?.Dispose(); - if (Current == this) - { - Current = null; - } - _disposed = true; - } - } + _disposed = true; + } + } +} + +/// +/// Options for Linux application initialization. +/// +public class LinuxApplicationOptions +{ + /// + /// Gets or sets the window title. + /// + public string? Title { get; set; } = "MAUI Application"; + + /// + /// Gets or sets the initial window width. + /// + public int Width { get; set; } = 800; + + /// + /// Gets or sets the initial window height. + /// + public int Height { get; set; } = 600; + + /// + /// Gets or sets whether to use hardware acceleration. + /// + public bool UseHardwareAcceleration { get; set; } = true; + + /// + /// Gets or sets the display server type. + /// + public DisplayServerType DisplayServer { get; set; } = DisplayServerType.Auto; + + /// + /// Gets or sets whether to force demo mode instead of loading the application's pages. + /// + public bool ForceDemo { get; set; } = false; +} + +/// +/// Display server type options. +/// +public enum DisplayServerType +{ + /// + /// Automatically detect the display server. + /// + Auto, + + /// + /// Use X11 (Xorg). + /// + X11, + + /// + /// Use Wayland. + /// + Wayland } diff --git a/LinuxApplicationOptions.cs b/LinuxApplicationOptions.cs deleted file mode 100644 index 3b5beff..0000000 --- a/LinuxApplicationOptions.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace Microsoft.Maui.Platform.Linux; - -public class LinuxApplicationOptions -{ - public string? Title { get; set; } = "MAUI Application"; - - public int Width { get; set; } = 800; - - public int Height { get; set; } = 600; - - public bool UseHardwareAcceleration { get; set; } = true; - - public DisplayServerType DisplayServer { get; set; } - - public bool ForceDemo { get; set; } - - public string? IconPath { get; set; } - - public bool UseGtk { get; set; } -} diff --git a/OpenMaui.Controls.Linux.csproj b/OpenMaui.Controls.Linux.csproj index c3d5c70..f4a601c 100644 --- a/OpenMaui.Controls.Linux.csproj +++ b/OpenMaui.Controls.Linux.csproj @@ -46,6 +46,9 @@ + + + diff --git a/Rendering/DirtyRectManager.cs b/Rendering/DirtyRectManager.cs index c8eb16e..c14ef79 100644 --- a/Rendering/DirtyRectManager.cs +++ b/Rendering/DirtyRectManager.cs @@ -1,254 +1,232 @@ -using System; -using System.Collections.Generic; -using System.Linq; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using SkiaSharp; namespace Microsoft.Maui.Platform.Linux.Rendering; +/// +/// Manages dirty rectangles for optimized rendering. +/// Only redraws areas that have been invalidated. +/// public class DirtyRectManager { - private readonly List _dirtyRects = new List(); + private readonly List _dirtyRects = new(); + private readonly object _lock = new(); + private bool _fullRedrawNeeded = true; + private SKRect _bounds; + private int _maxDirtyRects = 10; - private readonly object _lock = new object(); + /// + /// Gets or sets the maximum number of dirty rectangles to track before + /// falling back to a full redraw. + /// + public int MaxDirtyRects + { + get => _maxDirtyRects; + set => _maxDirtyRects = Math.Max(1, value); + } - private bool _fullRedrawNeeded = true; + /// + /// Gets whether a full redraw is needed. + /// + public bool NeedsFullRedraw => _fullRedrawNeeded; - private SKRect _bounds; + /// + /// Gets the current dirty rectangles. + /// + public IReadOnlyList DirtyRects + { + get + { + lock (_lock) + { + return _dirtyRects.ToList(); + } + } + } - private int _maxDirtyRects = 10; + /// + /// Gets whether there are any dirty regions. + /// + public bool HasDirtyRegions + { + get + { + lock (_lock) + { + return _fullRedrawNeeded || _dirtyRects.Count > 0; + } + } + } - public int MaxDirtyRects - { - get - { - return _maxDirtyRects; - } - set - { - _maxDirtyRects = Math.Max(1, value); - } - } + /// + /// Sets the rendering bounds. + /// + public void SetBounds(SKRect bounds) + { + if (_bounds != bounds) + { + _bounds = bounds; + InvalidateAll(); + } + } - public bool NeedsFullRedraw => _fullRedrawNeeded; + /// + /// Invalidates a specific region. + /// + public void Invalidate(SKRect rect) + { + if (rect.IsEmpty) return; - public IReadOnlyList DirtyRects - { - get - { - lock (_lock) - { - return _dirtyRects.ToList(); - } - } - } + lock (_lock) + { + if (_fullRedrawNeeded) return; - public bool HasDirtyRegions - { - get - { - lock (_lock) - { - return _fullRedrawNeeded || _dirtyRects.Count > 0; - } - } - } + // Clamp to bounds + rect = SKRect.Intersect(rect, _bounds); + if (rect.IsEmpty) return; - public void SetBounds(SKRect bounds) - { - //IL_0001: Unknown result type (might be due to invalid IL or missing references) - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - //IL_000f: Unknown result type (might be due to invalid IL or missing references) - //IL_0010: Unknown result type (might be due to invalid IL or missing references) - if (_bounds != bounds) - { - _bounds = bounds; - InvalidateAll(); - } - } + // Try to merge with existing dirty rects + for (int i = 0; i < _dirtyRects.Count; i++) + { + if (_dirtyRects[i].Contains(rect)) + { + // Already covered + return; + } - public void Invalidate(SKRect rect) - { - //IL_0028: Unknown result type (might be due to invalid IL or missing references) - //IL_002a: Unknown result type (might be due to invalid IL or missing references) - //IL_002f: Unknown result type (might be due to invalid IL or missing references) - //IL_0034: Unknown result type (might be due to invalid IL or missing references) - //IL_0052: Unknown result type (might be due to invalid IL or missing references) - //IL_0057: Unknown result type (might be due to invalid IL or missing references) - //IL_005b: Unknown result type (might be due to invalid IL or missing references) - //IL_014f: Unknown result type (might be due to invalid IL or missing references) - //IL_0071: Unknown result type (might be due to invalid IL or missing references) - //IL_009c: Unknown result type (might be due to invalid IL or missing references) - //IL_00a1: Unknown result type (might be due to invalid IL or missing references) - //IL_00a2: Unknown result type (might be due to invalid IL or missing references) - //IL_00a7: Unknown result type (might be due to invalid IL or missing references) - //IL_0084: Unknown result type (might be due to invalid IL or missing references) - //IL_00ca: Unknown result type (might be due to invalid IL or missing references) - //IL_00cf: Unknown result type (might be due to invalid IL or missing references) - //IL_00df: Unknown result type (might be due to invalid IL or missing references) - //IL_00e4: Unknown result type (might be due to invalid IL or missing references) - //IL_011c: Unknown result type (might be due to invalid IL or missing references) - //IL_0121: Unknown result type (might be due to invalid IL or missing references) - //IL_0122: Unknown result type (might be due to invalid IL or missing references) - if (((SKRect)(ref rect)).IsEmpty) - { - return; - } - lock (_lock) - { - if (_fullRedrawNeeded) - { - return; - } - rect = SKRect.Intersect(rect, _bounds); - if (((SKRect)(ref rect)).IsEmpty) - { - return; - } - for (int i = 0; i < _dirtyRects.Count; i++) - { - SKRect val = _dirtyRects[i]; - if (((SKRect)(ref val)).Contains(rect)) - { - return; - } - if (((SKRect)(ref rect)).Contains(_dirtyRects[i])) - { - _dirtyRects[i] = rect; - MergeDirtyRects(); - return; - } - SKRect val2 = SKRect.Intersect(_dirtyRects[i], rect); - if (!((SKRect)(ref val2)).IsEmpty) - { - float num = ((SKRect)(ref val2)).Width * ((SKRect)(ref val2)).Height; - val = _dirtyRects[i]; - float width = ((SKRect)(ref val)).Width; - val = _dirtyRects[i]; - float num2 = Math.Min(width * ((SKRect)(ref val)).Height, ((SKRect)(ref rect)).Width * ((SKRect)(ref rect)).Height); - if (num > num2 * 0.5f) - { - _dirtyRects[i] = SKRect.Union(_dirtyRects[i], rect); - MergeDirtyRects(); - return; - } - } - } - _dirtyRects.Add(rect); - if (_dirtyRects.Count > _maxDirtyRects) - { - _fullRedrawNeeded = true; - _dirtyRects.Clear(); - } - } - } + if (rect.Contains(_dirtyRects[i])) + { + // New rect covers existing + _dirtyRects[i] = rect; + MergeDirtyRects(); + return; + } - public void InvalidateAll() - { - lock (_lock) - { - _fullRedrawNeeded = true; - _dirtyRects.Clear(); - } - } + // Check if they overlap significantly (50% overlap) + var intersection = SKRect.Intersect(_dirtyRects[i], rect); + if (!intersection.IsEmpty) + { + float intersectArea = intersection.Width * intersection.Height; + float smallerArea = Math.Min( + _dirtyRects[i].Width * _dirtyRects[i].Height, + rect.Width * rect.Height); - public void Clear() - { - lock (_lock) - { - _fullRedrawNeeded = false; - _dirtyRects.Clear(); - } - } + if (intersectArea > smallerArea * 0.5f) + { + // Merge the rectangles + _dirtyRects[i] = SKRect.Union(_dirtyRects[i], rect); + MergeDirtyRects(); + return; + } + } + } - public SKRect GetCombinedDirtyRect() - { - //IL_0027: Unknown result type (might be due to invalid IL or missing references) - //IL_002c: Unknown result type (might be due to invalid IL or missing references) - //IL_0078: Unknown result type (might be due to invalid IL or missing references) - //IL_0036: Unknown result type (might be due to invalid IL or missing references) - //IL_003b: Unknown result type (might be due to invalid IL or missing references) - //IL_0041: Unknown result type (might be due to invalid IL or missing references) - //IL_004a: Unknown result type (might be due to invalid IL or missing references) - //IL_004f: Unknown result type (might be due to invalid IL or missing references) - //IL_0054: Unknown result type (might be due to invalid IL or missing references) - //IL_006a: Unknown result type (might be due to invalid IL or missing references) - //IL_006b: Unknown result type (might be due to invalid IL or missing references) - lock (_lock) - { - if (_fullRedrawNeeded || _dirtyRects.Count == 0) - { - return _bounds; - } - SKRect val = _dirtyRects[0]; - for (int i = 1; i < _dirtyRects.Count; i++) - { - val = SKRect.Union(val, _dirtyRects[i]); - } - return val; - } - } + // Add as new dirty rect + _dirtyRects.Add(rect); - public void ApplyClipping(SKCanvas canvas) - { - //IL_0028: Unknown result type (might be due to invalid IL or missing references) - //IL_002e: Expected O, but got Unknown - //IL_003e: Unknown result type (might be due to invalid IL or missing references) - //IL_0043: Unknown result type (might be due to invalid IL or missing references) - //IL_0046: Unknown result type (might be due to invalid IL or missing references) - lock (_lock) - { - if (_fullRedrawNeeded || _dirtyRects.Count == 0) - { - return; - } - SKPath val = new SKPath(); - try - { - foreach (SKRect dirtyRect in _dirtyRects) - { - val.AddRect(dirtyRect, (SKPathDirection)0); - } - canvas.ClipPath(val, (SKClipOperation)1, false); - } - finally - { - ((IDisposable)val)?.Dispose(); - } - } - } + // Check if we have too many dirty rects + if (_dirtyRects.Count > _maxDirtyRects) + { + // Fall back to full redraw + _fullRedrawNeeded = true; + _dirtyRects.Clear(); + } + } + } - private void MergeDirtyRects() - { - //IL_0013: Unknown result type (might be due to invalid IL or missing references) - //IL_001f: Unknown result type (might be due to invalid IL or missing references) - //IL_0024: Unknown result type (might be due to invalid IL or missing references) - //IL_0029: Unknown result type (might be due to invalid IL or missing references) - //IL_0041: Unknown result type (might be due to invalid IL or missing references) - //IL_004d: Unknown result type (might be due to invalid IL or missing references) - //IL_0052: Unknown result type (might be due to invalid IL or missing references) - bool flag; - do - { - flag = false; - for (int i = 0; i < _dirtyRects.Count - 1; i++) - { - for (int j = i + 1; j < _dirtyRects.Count; j++) - { - SKRect val = SKRect.Intersect(_dirtyRects[i], _dirtyRects[j]); - if (!((SKRect)(ref val)).IsEmpty) - { - _dirtyRects[i] = SKRect.Union(_dirtyRects[i], _dirtyRects[j]); - _dirtyRects.RemoveAt(j); - flag = true; - break; - } - } - if (flag) - { - break; - } - } - } - while (flag); - } + /// + /// Invalidates the entire rendering area. + /// + public void InvalidateAll() + { + lock (_lock) + { + _fullRedrawNeeded = true; + _dirtyRects.Clear(); + } + } + + /// + /// Clears all dirty regions after rendering. + /// + public void Clear() + { + lock (_lock) + { + _fullRedrawNeeded = false; + _dirtyRects.Clear(); + } + } + + /// + /// Gets the combined dirty region as a single rectangle. + /// + public SKRect GetCombinedDirtyRect() + { + lock (_lock) + { + if (_fullRedrawNeeded || _dirtyRects.Count == 0) + { + return _bounds; + } + + var combined = _dirtyRects[0]; + for (int i = 1; i < _dirtyRects.Count; i++) + { + combined = SKRect.Union(combined, _dirtyRects[i]); + } + return combined; + } + } + + /// + /// Applies dirty region clipping to a canvas. + /// + public void ApplyClipping(SKCanvas canvas) + { + lock (_lock) + { + if (_fullRedrawNeeded || _dirtyRects.Count == 0) + { + // No clipping needed for full redraw + return; + } + + // Create a path from all dirty rects + using var path = new SKPath(); + foreach (var rect in _dirtyRects) + { + path.AddRect(rect); + } + + canvas.ClipPath(path); + } + } + + private void MergeDirtyRects() + { + // Simple merge pass - could be optimized + bool merged; + do + { + merged = false; + for (int i = 0; i < _dirtyRects.Count - 1; i++) + { + for (int j = i + 1; j < _dirtyRects.Count; j++) + { + var intersection = SKRect.Intersect(_dirtyRects[i], _dirtyRects[j]); + if (!intersection.IsEmpty) + { + _dirtyRects[i] = SKRect.Union(_dirtyRects[i], _dirtyRects[j]); + _dirtyRects.RemoveAt(j); + merged = true; + break; + } + } + if (merged) break; + } + } while (merged); + } } diff --git a/Rendering/GpuRenderingEngine.cs b/Rendering/GpuRenderingEngine.cs deleted file mode 100644 index 3c6fc2c..0000000 --- a/Rendering/GpuRenderingEngine.cs +++ /dev/null @@ -1,376 +0,0 @@ -using System; -using System.Collections.Generic; -using Microsoft.Maui.Platform.Linux.Window; -using SkiaSharp; - -namespace Microsoft.Maui.Platform.Linux.Rendering; - -public class GpuRenderingEngine : IDisposable -{ - private readonly X11Window _window; - - private GRContext? _grContext; - - private GRBackendRenderTarget? _renderTarget; - - private SKSurface? _surface; - - private SKCanvas? _canvas; - - private bool _disposed; - - private bool _gpuAvailable; - - private int _width; - - private int _height; - - private SKBitmap? _softwareBitmap; - - private SKCanvas? _softwareCanvas; - - private readonly List _dirtyRegions = new List(); - - private readonly object _dirtyLock = new object(); - - private bool _fullRedrawNeeded = true; - - private const int MaxDirtyRegions = 32; - - public bool IsGpuAccelerated - { - get - { - if (_gpuAvailable) - { - return _grContext != null; - } - return false; - } - } - - public string BackendName - { - get - { - if (!IsGpuAccelerated) - { - return "Software"; - } - return "OpenGL"; - } - } - - public int Width => _width; - - public int Height => _height; - - public GpuRenderingEngine(X11Window window) - { - _window = window; - _width = window.Width; - _height = window.Height; - _gpuAvailable = TryInitializeGpu(); - if (!_gpuAvailable) - { - Console.WriteLine("[GpuRenderingEngine] GPU not available, using software rendering"); - InitializeSoftwareRendering(); - } - _window.Resized += OnWindowResized; - _window.Exposed += OnWindowExposed; - } - - private bool TryInitializeGpu() - { - try - { - GRGlInterface val = GRGlInterface.Create(); - if (val == null) - { - Console.WriteLine("[GpuRenderingEngine] Failed to create GL interface"); - return false; - } - _grContext = GRContext.CreateGl(val); - if (_grContext == null) - { - Console.WriteLine("[GpuRenderingEngine] Failed to create GR context"); - ((SKNativeObject)val).Dispose(); - return false; - } - CreateGpuSurface(); - Console.WriteLine("[GpuRenderingEngine] GPU acceleration enabled"); - return true; - } - catch (Exception ex) - { - Console.WriteLine("[GpuRenderingEngine] GPU initialization failed: " + ex.Message); - return false; - } - } - - private void CreateGpuSurface() - { - //IL_0058: Unknown result type (might be due to invalid IL or missing references) - //IL_0059: Unknown result type (might be due to invalid IL or missing references) - //IL_0063: Expected O, but got Unknown - if (_grContext != null) - { - GRBackendRenderTarget? renderTarget = _renderTarget; - if (renderTarget != null) - { - ((SKNativeObject)renderTarget).Dispose(); - } - SKSurface? surface = _surface; - if (surface != null) - { - ((SKNativeObject)surface).Dispose(); - } - int num = Math.Max(1, _width); - int num2 = Math.Max(1, _height); - GRGlFramebufferInfo val = default(GRGlFramebufferInfo); - ((GRGlFramebufferInfo)(ref val))._002Ector(0u, SkiaExtensions.ToGlSizedFormat((SKColorType)4)); - _renderTarget = new GRBackendRenderTarget(num, num2, 0, 8, val); - _surface = SKSurface.Create(_grContext, _renderTarget, (GRSurfaceOrigin)1, (SKColorType)4); - if (_surface == null) - { - Console.WriteLine("[GpuRenderingEngine] Failed to create GPU surface, falling back to software"); - _gpuAvailable = false; - InitializeSoftwareRendering(); - } - else - { - _canvas = _surface.Canvas; - } - } - } - - private void InitializeSoftwareRendering() - { - //IL_0048: Unknown result type (might be due to invalid IL or missing references) - //IL_0049: Unknown result type (might be due to invalid IL or missing references) - //IL_0053: Expected O, but got Unknown - //IL_005a: Unknown result type (might be due to invalid IL or missing references) - //IL_0064: Expected O, but got Unknown - int num = Math.Max(1, _width); - int num2 = Math.Max(1, _height); - SKBitmap? softwareBitmap = _softwareBitmap; - if (softwareBitmap != null) - { - ((SKNativeObject)softwareBitmap).Dispose(); - } - SKCanvas? softwareCanvas = _softwareCanvas; - if (softwareCanvas != null) - { - ((SKNativeObject)softwareCanvas).Dispose(); - } - SKImageInfo val = default(SKImageInfo); - ((SKImageInfo)(ref val))._002Ector(num, num2, (SKColorType)6, (SKAlphaType)2); - _softwareBitmap = new SKBitmap(val); - _softwareCanvas = new SKCanvas(_softwareBitmap); - _canvas = _softwareCanvas; - } - - private void OnWindowResized(object? sender, (int Width, int Height) size) - { - (_width, _height) = size; - if (_gpuAvailable && _grContext != null) - { - CreateGpuSurface(); - } - else - { - InitializeSoftwareRendering(); - } - _fullRedrawNeeded = true; - } - - private void OnWindowExposed(object? sender, EventArgs e) - { - _fullRedrawNeeded = true; - } - - public void InvalidateRegion(SKRect region) - { - //IL_0026: Unknown result type (might be due to invalid IL or missing references) - //IL_003f: Unknown result type (might be due to invalid IL or missing references) - //IL_0044: Unknown result type (might be due to invalid IL or missing references) - //IL_0049: Unknown result type (might be due to invalid IL or missing references) - //IL_008f: Unknown result type (might be due to invalid IL or missing references) - if (((SKRect)(ref region)).IsEmpty || ((SKRect)(ref region)).Width <= 0f || ((SKRect)(ref region)).Height <= 0f) - { - return; - } - region = SKRect.Intersect(region, new SKRect(0f, 0f, (float)Width, (float)Height)); - if (((SKRect)(ref region)).IsEmpty) - { - return; - } - lock (_dirtyLock) - { - if (_dirtyRegions.Count >= 32) - { - _fullRedrawNeeded = true; - _dirtyRegions.Clear(); - } - else - { - _dirtyRegions.Add(region); - } - } - } - - public void InvalidateAll() - { - _fullRedrawNeeded = true; - } - - public void Render(SkiaView rootView) - { - //IL_001f: Unknown result type (might be due to invalid IL or missing references) - //IL_0020: Unknown result type (might be due to invalid IL or missing references) - //IL_003f: Unknown result type (might be due to invalid IL or missing references) - //IL_0096: Unknown result type (might be due to invalid IL or missing references) - //IL_00e5: Unknown result type (might be due to invalid IL or missing references) - //IL_00ea: Unknown result type (might be due to invalid IL or missing references) - //IL_0110: Unknown result type (might be due to invalid IL or missing references) - //IL_0101: Unknown result type (might be due to invalid IL or missing references) - //IL_017a: Unknown result type (might be due to invalid IL or missing references) - if (_canvas == null) - { - return; - } - SKSize availableSize = default(SKSize); - ((SKSize)(ref availableSize))._002Ector((float)Width, (float)Height); - rootView.Measure(availableSize); - rootView.Arrange(new SKRect(0f, 0f, (float)Width, (float)Height)); - bool flag; - List list; - lock (_dirtyLock) - { - flag = _fullRedrawNeeded || _dirtyRegions.Count == 0; - if (flag) - { - list = new List - { - new SKRect(0f, 0f, (float)Width, (float)Height) - }; - _dirtyRegions.Clear(); - _fullRedrawNeeded = false; - } - else - { - list = new List(_dirtyRegions); - _dirtyRegions.Clear(); - } - } - foreach (SKRect item in list) - { - _canvas.Save(); - if (!flag) - { - _canvas.ClipRect(item, (SKClipOperation)1, false); - } - _canvas.Clear(SKColors.White); - rootView.Draw(_canvas); - _canvas.Restore(); - } - SkiaView.DrawPopupOverlays(_canvas); - if (LinuxDialogService.HasActiveDialog) - { - LinuxDialogService.DrawDialogs(_canvas, new SKRect(0f, 0f, (float)Width, (float)Height)); - } - _canvas.Flush(); - if (_gpuAvailable && _grContext != null) - { - _grContext.Submit(false); - } - else if (_softwareBitmap != null) - { - IntPtr pixels = _softwareBitmap.GetPixels(); - if (pixels != IntPtr.Zero) - { - _window.DrawPixels(pixels, Width, Height, _softwareBitmap.RowBytes); - } - } - } - - public GpuStats GetStats() - { - if (_grContext == null) - { - return new GpuStats - { - IsGpuAccelerated = false - }; - } - int num = default(int); - long resourceCacheLimitBytes = default(long); - _grContext.GetResourceCacheLimits(ref num, ref resourceCacheLimitBytes); - return new GpuStats - { - IsGpuAccelerated = true, - MaxTextureSize = 4096, - ResourceCacheUsedBytes = 0L, - ResourceCacheLimitBytes = resourceCacheLimitBytes - }; - } - - public void PurgeResources() - { - GRContext? grContext = _grContext; - if (grContext != null) - { - grContext.PurgeResources(); - } - } - - public SKCanvas? GetCanvas() - { - return _canvas; - } - - protected virtual void Dispose(bool disposing) - { - if (_disposed) - { - return; - } - if (disposing) - { - _window.Resized -= OnWindowResized; - _window.Exposed -= OnWindowExposed; - SKSurface? surface = _surface; - if (surface != null) - { - ((SKNativeObject)surface).Dispose(); - } - GRBackendRenderTarget? renderTarget = _renderTarget; - if (renderTarget != null) - { - ((SKNativeObject)renderTarget).Dispose(); - } - GRContext? grContext = _grContext; - if (grContext != null) - { - ((SKNativeObject)grContext).Dispose(); - } - SKBitmap? softwareBitmap = _softwareBitmap; - if (softwareBitmap != null) - { - ((SKNativeObject)softwareBitmap).Dispose(); - } - SKCanvas? softwareCanvas = _softwareCanvas; - if (softwareCanvas != null) - { - ((SKNativeObject)softwareCanvas).Dispose(); - } - } - _disposed = true; - } - - public void Dispose() - { - Dispose(disposing: true); - GC.SuppressFinalize(this); - } -} diff --git a/Rendering/GpuStats.cs b/Rendering/GpuStats.cs deleted file mode 100644 index b24e8b7..0000000 --- a/Rendering/GpuStats.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace Microsoft.Maui.Platform.Linux.Rendering; - -public class GpuStats -{ - public bool IsGpuAccelerated { get; init; } - - public int MaxTextureSize { get; init; } - - public long ResourceCacheUsedBytes { get; init; } - - public long ResourceCacheLimitBytes { get; init; } - - public double ResourceCacheUsedMB => (double)ResourceCacheUsedBytes / 1048576.0; - - public double ResourceCacheLimitMB => (double)ResourceCacheLimitBytes / 1048576.0; -} diff --git a/Rendering/GtkSkiaSurfaceWidget.cs b/Rendering/GtkSkiaSurfaceWidget.cs deleted file mode 100644 index ecedc2f..0000000 --- a/Rendering/GtkSkiaSurfaceWidget.cs +++ /dev/null @@ -1,455 +0,0 @@ -using System; -using System.Runtime.InteropServices; -using Microsoft.Maui.Platform.Linux.Native; -using SkiaSharp; - -namespace Microsoft.Maui.Platform.Linux.Rendering; - -public sealed class GtkSkiaSurfaceWidget : IDisposable -{ - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - private delegate bool DrawCallback(IntPtr widget, IntPtr cairoContext, IntPtr userData); - - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - private delegate bool ConfigureCallback(IntPtr widget, IntPtr eventData, IntPtr userData); - - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - private delegate bool ButtonEventCallback(IntPtr widget, IntPtr eventData, IntPtr userData); - - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - private delegate bool MotionEventCallback(IntPtr widget, IntPtr eventData, IntPtr userData); - - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - private delegate bool KeyEventCallback(IntPtr widget, IntPtr eventData, IntPtr userData); - - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - private delegate bool ScrollEventCallback(IntPtr widget, IntPtr eventData, IntPtr userData); - - private struct GdkEventButton - { - public int type; - - public IntPtr window; - - public sbyte send_event; - - public uint time; - - public double x; - - public double y; - - public IntPtr axes; - - public uint state; - - public uint button; - } - - private struct GdkEventMotion - { - public int type; - - public IntPtr window; - - public sbyte send_event; - - public uint time; - - public double x; - - public double y; - } - - private struct GdkEventKey - { - public int type; - - public IntPtr window; - - public sbyte send_event; - - public uint time; - - public uint state; - - public uint keyval; - - public int length; - - public IntPtr str; - - public ushort hardware_keycode; - } - - private struct GdkEventScroll - { - public int type; - - public IntPtr window; - - public sbyte send_event; - - public uint time; - - public double x; - - public double y; - - public uint state; - - public int direction; - - public IntPtr device; - - public double x_root; - - public double y_root; - - public double delta_x; - - public double delta_y; - } - - private IntPtr _widget; - - private SKImageInfo _imageInfo; - - private SKBitmap? _bitmap; - - private SKCanvas? _canvas; - - private IntPtr _cairoSurface; - - private readonly DrawCallback _drawCallback; - - private readonly ConfigureCallback _configureCallback; - - private ulong _drawSignalId; - - private ulong _configureSignalId; - - private bool _isTransparent; - - private readonly ButtonEventCallback _buttonPressCallback; - - private readonly ButtonEventCallback _buttonReleaseCallback; - - private readonly MotionEventCallback _motionCallback; - - private readonly KeyEventCallback _keyPressCallback; - - private readonly KeyEventCallback _keyReleaseCallback; - - private readonly ScrollEventCallback _scrollCallback; - - public IntPtr Widget => _widget; - - public SKCanvas? Canvas => _canvas; - - public SKImageInfo ImageInfo => _imageInfo; - - public int Width => ((SKImageInfo)(ref _imageInfo)).Width; - - public int Height => ((SKImageInfo)(ref _imageInfo)).Height; - - public bool IsTransparent => _isTransparent; - - public event EventHandler? DrawRequested; - - public event EventHandler<(int Width, int Height)>? Resized; - - public event EventHandler<(double X, double Y, int Button)>? PointerPressed; - - public event EventHandler<(double X, double Y, int Button)>? PointerReleased; - - public event EventHandler<(double X, double Y)>? PointerMoved; - - public event EventHandler<(uint KeyVal, uint KeyCode, uint State)>? KeyPressed; - - public event EventHandler<(uint KeyVal, uint KeyCode, uint State)>? KeyReleased; - - public event EventHandler<(double X, double Y, double DeltaX, double DeltaY)>? Scrolled; - - public event EventHandler? TextInput; - - public GtkSkiaSurfaceWidget(int width, int height) - { - _widget = GtkNative.gtk_drawing_area_new(); - if (_widget == IntPtr.Zero) - { - throw new InvalidOperationException("Failed to create GTK drawing area"); - } - GtkNative.gtk_widget_set_size_request(_widget, width, height); - GtkNative.gtk_widget_add_events(_widget, 10551046); - GtkNative.gtk_widget_set_can_focus(_widget, canFocus: true); - CreateBuffer(width, height); - _drawCallback = OnDraw; - _configureCallback = OnConfigure; - _buttonPressCallback = OnButtonPress; - _buttonReleaseCallback = OnButtonRelease; - _motionCallback = OnMotion; - _keyPressCallback = OnKeyPress; - _keyReleaseCallback = OnKeyRelease; - _scrollCallback = OnScroll; - _drawSignalId = GtkNative.g_signal_connect_data(_widget, "draw", Marshal.GetFunctionPointerForDelegate(_drawCallback), IntPtr.Zero, IntPtr.Zero, 0); - _configureSignalId = GtkNative.g_signal_connect_data(_widget, "configure-event", Marshal.GetFunctionPointerForDelegate(_configureCallback), IntPtr.Zero, IntPtr.Zero, 0); - GtkNative.g_signal_connect_data(_widget, "button-press-event", Marshal.GetFunctionPointerForDelegate(_buttonPressCallback), IntPtr.Zero, IntPtr.Zero, 0); - GtkNative.g_signal_connect_data(_widget, "button-release-event", Marshal.GetFunctionPointerForDelegate(_buttonReleaseCallback), IntPtr.Zero, IntPtr.Zero, 0); - GtkNative.g_signal_connect_data(_widget, "motion-notify-event", Marshal.GetFunctionPointerForDelegate(_motionCallback), IntPtr.Zero, IntPtr.Zero, 0); - GtkNative.g_signal_connect_data(_widget, "key-press-event", Marshal.GetFunctionPointerForDelegate(_keyPressCallback), IntPtr.Zero, IntPtr.Zero, 0); - GtkNative.g_signal_connect_data(_widget, "key-release-event", Marshal.GetFunctionPointerForDelegate(_keyReleaseCallback), IntPtr.Zero, IntPtr.Zero, 0); - GtkNative.g_signal_connect_data(_widget, "scroll-event", Marshal.GetFunctionPointerForDelegate(_scrollCallback), IntPtr.Zero, IntPtr.Zero, 0); - Console.WriteLine($"[GtkSkiaSurfaceWidget] Created with size {width}x{height}"); - } - - private void CreateBuffer(int width, int height) - { - //IL_005c: Unknown result type (might be due to invalid IL or missing references) - //IL_0061: Unknown result type (might be due to invalid IL or missing references) - //IL_0068: Unknown result type (might be due to invalid IL or missing references) - //IL_006d: Unknown result type (might be due to invalid IL or missing references) - //IL_0077: Expected O, but got Unknown - //IL_007e: Unknown result type (might be due to invalid IL or missing references) - //IL_0088: Expected O, but got Unknown - width = Math.Max(1, width); - height = Math.Max(1, height); - SKCanvas? canvas = _canvas; - if (canvas != null) - { - ((SKNativeObject)canvas).Dispose(); - } - SKBitmap? bitmap = _bitmap; - if (bitmap != null) - { - ((SKNativeObject)bitmap).Dispose(); - } - if (_cairoSurface != IntPtr.Zero) - { - CairoNative.cairo_surface_destroy(_cairoSurface); - _cairoSurface = IntPtr.Zero; - } - _imageInfo = new SKImageInfo(width, height, (SKColorType)6, (SKAlphaType)2); - _bitmap = new SKBitmap(_imageInfo); - _canvas = new SKCanvas(_bitmap); - IntPtr pixels = _bitmap.GetPixels(); - _cairoSurface = CairoNative.cairo_image_surface_create_for_data(pixels, CairoNative.cairo_format_t.CAIRO_FORMAT_ARGB32, ((SKImageInfo)(ref _imageInfo)).Width, ((SKImageInfo)(ref _imageInfo)).Height, ((SKImageInfo)(ref _imageInfo)).RowBytes); - Console.WriteLine($"[GtkSkiaSurfaceWidget] Created buffer {width}x{height}, stride={((SKImageInfo)(ref _imageInfo)).RowBytes}"); - } - - public void Resize(int width, int height) - { - if (width != ((SKImageInfo)(ref _imageInfo)).Width || height != ((SKImageInfo)(ref _imageInfo)).Height) - { - CreateBuffer(width, height); - this.Resized?.Invoke(this, (width, height)); - } - } - - public void RenderFrame(Action render) - { - //IL_0019: Unknown result type (might be due to invalid IL or missing references) - if (_canvas != null && _bitmap != null) - { - render(_canvas, _imageInfo); - _canvas.Flush(); - CairoNative.cairo_surface_flush(_cairoSurface); - CairoNative.cairo_surface_mark_dirty(_cairoSurface); - GtkNative.gtk_widget_queue_draw(_widget); - } - } - - public void Invalidate() - { - GtkNative.gtk_widget_queue_draw(_widget); - } - - public void SetTransparent(bool transparent) - { - _isTransparent = transparent; - } - - private bool OnDraw(IntPtr widget, IntPtr cairoContext, IntPtr userData) - { - //IL_002b: Unknown result type (might be due to invalid IL or missing references) - if (_cairoSurface == IntPtr.Zero || cairoContext == IntPtr.Zero) - { - return false; - } - if (_isTransparent) - { - SKCanvas? canvas = _canvas; - if (canvas != null) - { - canvas.Clear(SKColors.Transparent); - } - } - this.DrawRequested?.Invoke(this, EventArgs.Empty); - SKCanvas? canvas2 = _canvas; - if (canvas2 != null) - { - canvas2.Flush(); - } - CairoNative.cairo_surface_flush(_cairoSurface); - CairoNative.cairo_surface_mark_dirty(_cairoSurface); - CairoNative.cairo_set_source_surface(cairoContext, _cairoSurface, 0.0, 0.0); - CairoNative.cairo_paint(cairoContext); - return true; - } - - private bool OnConfigure(IntPtr widget, IntPtr eventData, IntPtr userData) - { - GtkNative.gtk_widget_get_allocation(widget, out var allocation); - if (allocation.Width > 0 && allocation.Height > 0 && (allocation.Width != ((SKImageInfo)(ref _imageInfo)).Width || allocation.Height != ((SKImageInfo)(ref _imageInfo)).Height)) - { - Resize(allocation.Width, allocation.Height); - } - return false; - } - - private bool OnButtonPress(IntPtr widget, IntPtr eventData, IntPtr userData) - { - GtkNative.gtk_widget_grab_focus(_widget); - var (num, num2, num3) = ParseButtonEvent(eventData); - Console.WriteLine($"[GtkSkiaSurfaceWidget] ButtonPress at ({num}, {num2}), button={num3}"); - this.PointerPressed?.Invoke(this, (num, num2, num3)); - return true; - } - - private bool OnButtonRelease(IntPtr widget, IntPtr eventData, IntPtr userData) - { - var (item, item2, item3) = ParseButtonEvent(eventData); - this.PointerReleased?.Invoke(this, (item, item2, item3)); - return true; - } - - private bool OnMotion(IntPtr widget, IntPtr eventData, IntPtr userData) - { - var (item, item2) = ParseMotionEvent(eventData); - this.PointerMoved?.Invoke(this, (item, item2)); - return true; - } - - public void RaisePointerPressed(double x, double y, int button) - { - Console.WriteLine($"[GtkSkiaSurfaceWidget] RaisePointerPressed at ({x}, {y}), button={button}"); - this.PointerPressed?.Invoke(this, (x, y, button)); - } - - public void RaisePointerReleased(double x, double y, int button) - { - this.PointerReleased?.Invoke(this, (x, y, button)); - } - - public void RaisePointerMoved(double x, double y) - { - this.PointerMoved?.Invoke(this, (x, y)); - } - - private bool OnKeyPress(IntPtr widget, IntPtr eventData, IntPtr userData) - { - var (num, item, item2) = ParseKeyEvent(eventData); - this.KeyPressed?.Invoke(this, (num, item, item2)); - uint num2 = GdkNative.gdk_keyval_to_unicode(num); - if (num2 != 0 && num2 < 65536) - { - char c = (char)num2; - if (!char.IsControl(c) || c == '\r' || c == '\n' || c == '\t') - { - string text = c.ToString(); - Console.WriteLine($"[GtkSkiaSurfaceWidget] TextInput: '{text}' (keyval={num}, unicode={num2})"); - this.TextInput?.Invoke(this, text); - } - } - return true; - } - - private bool OnKeyRelease(IntPtr widget, IntPtr eventData, IntPtr userData) - { - var (item, item2, item3) = ParseKeyEvent(eventData); - this.KeyReleased?.Invoke(this, (item, item2, item3)); - return true; - } - - private bool OnScroll(IntPtr widget, IntPtr eventData, IntPtr userData) - { - var (item, item2, item3, item4) = ParseScrollEvent(eventData); - this.Scrolled?.Invoke(this, (item, item2, item3, item4)); - return true; - } - - private static (double x, double y, int button) ParseButtonEvent(IntPtr eventData) - { - GdkEventButton gdkEventButton = Marshal.PtrToStructure(eventData); - return (x: gdkEventButton.x, y: gdkEventButton.y, button: (int)gdkEventButton.button); - } - - private static (double x, double y) ParseMotionEvent(IntPtr eventData) - { - GdkEventMotion gdkEventMotion = Marshal.PtrToStructure(eventData); - return (x: gdkEventMotion.x, y: gdkEventMotion.y); - } - - private static (uint keyval, uint keycode, uint state) ParseKeyEvent(IntPtr eventData) - { - GdkEventKey gdkEventKey = Marshal.PtrToStructure(eventData); - return (keyval: gdkEventKey.keyval, keycode: gdkEventKey.hardware_keycode, state: gdkEventKey.state); - } - - private static (double x, double y, double deltaX, double deltaY) ParseScrollEvent(IntPtr eventData) - { - GdkEventScroll gdkEventScroll = Marshal.PtrToStructure(eventData); - double item = 0.0; - double item2 = 0.0; - if (gdkEventScroll.direction == 4) - { - item = gdkEventScroll.delta_x; - item2 = gdkEventScroll.delta_y; - } - else - { - switch (gdkEventScroll.direction) - { - case 0: - item2 = -1.0; - break; - case 1: - item2 = 1.0; - break; - case 2: - item = -1.0; - break; - case 3: - item = 1.0; - break; - } - } - return (x: gdkEventScroll.x, y: gdkEventScroll.y, deltaX: item, deltaY: item2); - } - - public void GrabFocus() - { - GtkNative.gtk_widget_grab_focus(_widget); - } - - public void Dispose() - { - SKCanvas? canvas = _canvas; - if (canvas != null) - { - ((SKNativeObject)canvas).Dispose(); - } - _canvas = null; - SKBitmap? bitmap = _bitmap; - if (bitmap != null) - { - ((SKNativeObject)bitmap).Dispose(); - } - _bitmap = null; - if (_cairoSurface != IntPtr.Zero) - { - CairoNative.cairo_surface_destroy(_cairoSurface); - _cairoSurface = IntPtr.Zero; - } - } -} diff --git a/Rendering/LayeredRenderer.cs b/Rendering/LayeredRenderer.cs deleted file mode 100644 index 25a6a54..0000000 --- a/Rendering/LayeredRenderer.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using SkiaSharp; - -namespace Microsoft.Maui.Platform.Linux.Rendering; - -public class LayeredRenderer : IDisposable -{ - private readonly Dictionary _layers = new Dictionary(); - - private readonly object _lock = new object(); - - private bool _disposed; - - public RenderLayer GetLayer(int zIndex) - { - lock (_lock) - { - if (!_layers.TryGetValue(zIndex, out RenderLayer value)) - { - value = new RenderLayer(zIndex); - _layers[zIndex] = value; - } - return value; - } - } - - public void RemoveLayer(int zIndex) - { - lock (_lock) - { - if (_layers.TryGetValue(zIndex, out RenderLayer value)) - { - value.Dispose(); - _layers.Remove(zIndex); - } - } - } - - public void Composite(SKCanvas canvas, SKRect bounds) - { - //IL_004f: Unknown result type (might be due to invalid IL or missing references) - lock (_lock) - { - foreach (RenderLayer item in _layers.Values.OrderBy((RenderLayer l) => l.ZIndex)) - { - item.DrawTo(canvas, bounds); - } - } - } - - public void InvalidateAll() - { - lock (_lock) - { - foreach (RenderLayer value in _layers.Values) - { - value.Invalidate(); - } - } - } - - public void Dispose() - { - if (_disposed) - { - return; - } - _disposed = true; - lock (_lock) - { - foreach (RenderLayer value in _layers.Values) - { - value.Dispose(); - } - _layers.Clear(); - } - } -} diff --git a/Rendering/RenderCache.cs b/Rendering/RenderCache.cs index cb34aed..01b3352 100644 --- a/Rendering/RenderCache.cs +++ b/Rendering/RenderCache.cs @@ -1,230 +1,526 @@ -using System; -using System.Collections.Generic; -using System.Linq; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using SkiaSharp; namespace Microsoft.Maui.Platform.Linux.Rendering; +/// +/// Caches rendered content for views that don't change frequently. +/// Improves performance by avoiding redundant rendering. +/// public class RenderCache : IDisposable { - private class CacheEntry - { - public string Key { get; set; } = string.Empty; + private readonly Dictionary _cache = new(); + private readonly object _lock = new(); + private long _maxCacheSize = 50 * 1024 * 1024; // 50 MB default + private long _currentCacheSize; + private bool _disposed; - public SKBitmap? Bitmap { get; set; } + /// + /// Gets or sets the maximum cache size in bytes. + /// + public long MaxCacheSize + { + get => _maxCacheSize; + set + { + _maxCacheSize = Math.Max(1024 * 1024, value); // Minimum 1 MB + TrimCache(); + } + } - public long Size { get; set; } + /// + /// Gets the current cache size in bytes. + /// + public long CurrentCacheSize => _currentCacheSize; - public DateTime Created { get; set; } + /// + /// Gets the number of cached items. + /// + public int CachedItemCount + { + get + { + lock (_lock) + { + return _cache.Count; + } + } + } - public DateTime LastAccessed { get; set; } + /// + /// Tries to get a cached bitmap for the given key. + /// + public bool TryGet(string key, out SKBitmap? bitmap) + { + lock (_lock) + { + if (_cache.TryGetValue(key, out var entry)) + { + entry.LastAccessed = DateTime.UtcNow; + entry.AccessCount++; + bitmap = entry.Bitmap; + return true; + } + } - public int AccessCount { get; set; } - } + bitmap = null; + return false; + } - private readonly Dictionary _cache = new Dictionary(); + /// + /// Caches a bitmap with the given key. + /// + public void Set(string key, SKBitmap bitmap) + { + if (bitmap == null) return; - private readonly object _lock = new object(); + long bitmapSize = bitmap.ByteCount; - private long _maxCacheSize = 52428800L; + // Don't cache if bitmap is larger than max size + if (bitmapSize > _maxCacheSize) + { + return; + } - private long _currentCacheSize; + lock (_lock) + { + // Remove existing entry if present + if (_cache.TryGetValue(key, out var existing)) + { + _currentCacheSize -= existing.Size; + existing.Bitmap?.Dispose(); + } - private bool _disposed; + // Create copy of bitmap for cache + var cachedBitmap = bitmap.Copy(); + if (cachedBitmap == null) return; - public long MaxCacheSize - { - get - { - return _maxCacheSize; - } - set - { - _maxCacheSize = Math.Max(1048576L, value); - TrimCache(); - } - } + var entry = new CacheEntry + { + Key = key, + Bitmap = cachedBitmap, + Size = bitmapSize, + Created = DateTime.UtcNow, + LastAccessed = DateTime.UtcNow, + AccessCount = 1 + }; - public long CurrentCacheSize => _currentCacheSize; + _cache[key] = entry; + _currentCacheSize += bitmapSize; - public int CachedItemCount - { - get - { - lock (_lock) - { - return _cache.Count; - } - } - } + // Trim cache if needed + TrimCache(); + } + } - public bool TryGet(string key, out SKBitmap? bitmap) - { - lock (_lock) - { - if (_cache.TryGetValue(key, out CacheEntry value)) - { - value.LastAccessed = DateTime.UtcNow; - value.AccessCount++; - bitmap = value.Bitmap; - return true; - } - } - bitmap = null; - return false; - } + /// + /// Invalidates a cached entry. + /// + public void Invalidate(string key) + { + lock (_lock) + { + if (_cache.TryGetValue(key, out var entry)) + { + _currentCacheSize -= entry.Size; + entry.Bitmap?.Dispose(); + _cache.Remove(key); + } + } + } - public void Set(string key, SKBitmap bitmap) - { - if (bitmap == null) - { - return; - } - long num = bitmap.ByteCount; - if (num > _maxCacheSize) - { - return; - } - lock (_lock) - { - if (_cache.TryGetValue(key, out CacheEntry value)) - { - _currentCacheSize -= value.Size; - SKBitmap? bitmap2 = value.Bitmap; - if (bitmap2 != null) - { - ((SKNativeObject)bitmap2).Dispose(); - } - } - SKBitmap val = bitmap.Copy(); - if (val != null) - { - CacheEntry value2 = new CacheEntry - { - Key = key, - Bitmap = val, - Size = num, - Created = DateTime.UtcNow, - LastAccessed = DateTime.UtcNow, - AccessCount = 1 - }; - _cache[key] = value2; - _currentCacheSize += num; - TrimCache(); - } - } - } + /// + /// Invalidates all entries matching a prefix. + /// + public void InvalidatePrefix(string prefix) + { + lock (_lock) + { + var keysToRemove = _cache.Keys + .Where(k => k.StartsWith(prefix, StringComparison.Ordinal)) + .ToList(); - public void Invalidate(string key) - { - lock (_lock) - { - if (_cache.TryGetValue(key, out CacheEntry value)) - { - _currentCacheSize -= value.Size; - SKBitmap? bitmap = value.Bitmap; - if (bitmap != null) - { - ((SKNativeObject)bitmap).Dispose(); - } - _cache.Remove(key); - } - } - } + foreach (var key in keysToRemove) + { + if (_cache.TryGetValue(key, out var entry)) + { + _currentCacheSize -= entry.Size; + entry.Bitmap?.Dispose(); + _cache.Remove(key); + } + } + } + } - public void InvalidatePrefix(string prefix) - { - lock (_lock) - { - foreach (string item in _cache.Keys.Where((string k) => k.StartsWith(prefix, StringComparison.Ordinal)).ToList()) - { - if (_cache.TryGetValue(item, out CacheEntry value)) - { - _currentCacheSize -= value.Size; - SKBitmap? bitmap = value.Bitmap; - if (bitmap != null) - { - ((SKNativeObject)bitmap).Dispose(); - } - _cache.Remove(item); - } - } - } - } + /// + /// Clears all cached content. + /// + public void Clear() + { + lock (_lock) + { + foreach (var entry in _cache.Values) + { + entry.Bitmap?.Dispose(); + } + _cache.Clear(); + _currentCacheSize = 0; + } + } - public void Clear() - { - lock (_lock) - { - foreach (CacheEntry value in _cache.Values) - { - SKBitmap? bitmap = value.Bitmap; - if (bitmap != null) - { - ((SKNativeObject)bitmap).Dispose(); - } - } - _cache.Clear(); - _currentCacheSize = 0L; - } - } + /// + /// Renders content with caching. + /// + public SKBitmap GetOrCreate(string key, int width, int height, Action render) + { + // Check cache first + if (TryGet(key, out var cached) && cached != null && + cached.Width == width && cached.Height == height) + { + return cached; + } - public SKBitmap GetOrCreate(string key, int width, int height, Action render) - { - //IL_0026: Unknown result type (might be due to invalid IL or missing references) - //IL_002c: Expected O, but got Unknown - //IL_002d: Unknown result type (might be due to invalid IL or missing references) - //IL_0033: Expected O, but got Unknown - //IL_0034: Unknown result type (might be due to invalid IL or missing references) - if (TryGet(key, out SKBitmap bitmap) && bitmap != null && bitmap.Width == width && bitmap.Height == height) - { - return bitmap; - } - SKBitmap val = new SKBitmap(width, height, (SKColorType)4, (SKAlphaType)2); - SKCanvas val2 = new SKCanvas(val); - try - { - val2.Clear(SKColors.Transparent); - render(val2); - } - finally - { - ((IDisposable)val2)?.Dispose(); - } - Set(key, val); - return val; - } + // Create new bitmap + var bitmap = new SKBitmap(width, height, SKColorType.Rgba8888, SKAlphaType.Premul); + using (var canvas = new SKCanvas(bitmap)) + { + canvas.Clear(SKColors.Transparent); + render(canvas); + } - private void TrimCache() - { - if (_currentCacheSize <= _maxCacheSize) - { - return; - } - foreach (CacheEntry item in (from e in _cache.Values - orderby e.LastAccessed, e.AccessCount - select e).ToList()) - { - if ((double)_currentCacheSize <= (double)_maxCacheSize * 0.8) - { - break; - } - _currentCacheSize -= item.Size; - SKBitmap? bitmap = item.Bitmap; - if (bitmap != null) - { - ((SKNativeObject)bitmap).Dispose(); - } - _cache.Remove(item.Key); - } - } + // Cache it + Set(key, bitmap); - public void Dispose() - { - if (!_disposed) - { - _disposed = true; - Clear(); - } - } + return bitmap; + } + + private void TrimCache() + { + if (_currentCacheSize <= _maxCacheSize) return; + + // Remove least recently used entries until under limit + var entries = _cache.Values + .OrderBy(e => e.LastAccessed) + .ThenBy(e => e.AccessCount) + .ToList(); + + foreach (var entry in entries) + { + if (_currentCacheSize <= _maxCacheSize * 0.8) // Target 80% usage + { + break; + } + + _currentCacheSize -= entry.Size; + entry.Bitmap?.Dispose(); + _cache.Remove(entry.Key); + } + } + + public void Dispose() + { + if (_disposed) return; + _disposed = true; + Clear(); + } + + private class CacheEntry + { + public string Key { get; set; } = string.Empty; + public SKBitmap? Bitmap { get; set; } + public long Size { get; set; } + public DateTime Created { get; set; } + public DateTime LastAccessed { get; set; } + public int AccessCount { get; set; } + } +} + +/// +/// Provides layered rendering for separating static and dynamic content. +/// +public class LayeredRenderer : IDisposable +{ + private readonly Dictionary _layers = new(); + private readonly object _lock = new(); + private bool _disposed; + + /// + /// Gets or creates a render layer. + /// + public RenderLayer GetLayer(int zIndex) + { + lock (_lock) + { + if (!_layers.TryGetValue(zIndex, out var layer)) + { + layer = new RenderLayer(zIndex); + _layers[zIndex] = layer; + } + return layer; + } + } + + /// + /// Removes a render layer. + /// + public void RemoveLayer(int zIndex) + { + lock (_lock) + { + if (_layers.TryGetValue(zIndex, out var layer)) + { + layer.Dispose(); + _layers.Remove(zIndex); + } + } + } + + /// + /// Composites all layers onto the target canvas. + /// + public void Composite(SKCanvas canvas, SKRect bounds) + { + lock (_lock) + { + foreach (var layer in _layers.Values.OrderBy(l => l.ZIndex)) + { + layer.DrawTo(canvas, bounds); + } + } + } + + /// + /// Invalidates all layers. + /// + public void InvalidateAll() + { + lock (_lock) + { + foreach (var layer in _layers.Values) + { + layer.Invalidate(); + } + } + } + + public void Dispose() + { + if (_disposed) return; + _disposed = true; + + lock (_lock) + { + foreach (var layer in _layers.Values) + { + layer.Dispose(); + } + _layers.Clear(); + } + } +} + +/// +/// Represents a single render layer with its own bitmap buffer. +/// +public class RenderLayer : IDisposable +{ + private SKBitmap? _bitmap; + private SKCanvas? _canvas; + private bool _isDirty = true; + private SKRect _bounds; + private bool _disposed; + + /// + /// Gets the Z-index of this layer. + /// + public int ZIndex { get; } + + /// + /// Gets whether this layer needs to be redrawn. + /// + public bool IsDirty => _isDirty; + + /// + /// Gets or sets whether this layer is visible. + /// + public bool IsVisible { get; set; } = true; + + /// + /// Gets or sets the layer opacity (0-1). + /// + public float Opacity { get; set; } = 1f; + + public RenderLayer(int zIndex) + { + ZIndex = zIndex; + } + + /// + /// Prepares the layer for rendering. + /// + public SKCanvas BeginDraw(SKRect bounds) + { + if (_bitmap == null || _bounds != bounds) + { + _bitmap?.Dispose(); + _canvas?.Dispose(); + + int width = Math.Max(1, (int)bounds.Width); + int height = Math.Max(1, (int)bounds.Height); + + _bitmap = new SKBitmap(width, height, SKColorType.Rgba8888, SKAlphaType.Premul); + _canvas = new SKCanvas(_bitmap); + _bounds = bounds; + } + + _canvas!.Clear(SKColors.Transparent); + _isDirty = false; + return _canvas; + } + + /// + /// Marks the layer as needing redraw. + /// + public void Invalidate() + { + _isDirty = true; + } + + /// + /// Draws this layer to the target canvas. + /// + public void DrawTo(SKCanvas canvas, SKRect bounds) + { + if (!IsVisible || _bitmap == null) return; + + using var paint = new SKPaint + { + Color = SKColors.White.WithAlpha((byte)(Opacity * 255)) + }; + + canvas.DrawBitmap(_bitmap, bounds.Left, bounds.Top, paint); + } + + public void Dispose() + { + if (_disposed) return; + _disposed = true; + + _canvas?.Dispose(); + _bitmap?.Dispose(); + } +} + +/// +/// Provides text rendering optimization with glyph caching. +/// +public class TextRenderCache : IDisposable +{ + private readonly Dictionary _cache = new(); + private readonly object _lock = new(); + private int _maxEntries = 500; + private bool _disposed; + + /// + /// Gets or sets the maximum number of cached text entries. + /// + public int MaxEntries + { + get => _maxEntries; + set => _maxEntries = Math.Max(10, value); + } + + /// + /// Gets a cached text bitmap or creates one. + /// + public SKBitmap GetOrCreate(string text, SKPaint paint) + { + var key = new TextCacheKey(text, paint); + + lock (_lock) + { + if (_cache.TryGetValue(key, out var cached)) + { + return cached; + } + + // Create text bitmap + var bounds = new SKRect(); + paint.MeasureText(text, ref bounds); + + int width = Math.Max(1, (int)Math.Ceiling(bounds.Width) + 2); + int height = Math.Max(1, (int)Math.Ceiling(bounds.Height) + 2); + + var bitmap = new SKBitmap(width, height, SKColorType.Rgba8888, SKAlphaType.Premul); + using (var canvas = new SKCanvas(bitmap)) + { + canvas.Clear(SKColors.Transparent); + canvas.DrawText(text, -bounds.Left + 1, -bounds.Top + 1, paint); + } + + // Trim cache if needed + if (_cache.Count >= _maxEntries) + { + var oldest = _cache.First(); + oldest.Value.Dispose(); + _cache.Remove(oldest.Key); + } + + _cache[key] = bitmap; + return bitmap; + } + } + + /// + /// Clears all cached text. + /// + public void Clear() + { + lock (_lock) + { + foreach (var entry in _cache.Values) + { + entry.Dispose(); + } + _cache.Clear(); + } + } + + public void Dispose() + { + if (_disposed) return; + _disposed = true; + Clear(); + } + + private readonly struct TextCacheKey : IEquatable + { + private readonly string _text; + private readonly float _textSize; + private readonly SKColor _color; + private readonly int _weight; + private readonly int _hashCode; + + public TextCacheKey(string text, SKPaint paint) + { + _text = text; + _textSize = paint.TextSize; + _color = paint.Color; + _weight = paint.Typeface?.FontWeight ?? (int)SKFontStyleWeight.Normal; + _hashCode = HashCode.Combine(_text, _textSize, _color, _weight); + } + + public bool Equals(TextCacheKey other) + { + return _text == other._text && + Math.Abs(_textSize - other._textSize) < 0.001f && + _color == other._color && + _weight == other._weight; + } + + public override bool Equals(object? obj) => obj is TextCacheKey other && Equals(other); + public override int GetHashCode() => _hashCode; + } } diff --git a/Rendering/RenderLayer.cs b/Rendering/RenderLayer.cs deleted file mode 100644 index f8599c8..0000000 --- a/Rendering/RenderLayer.cs +++ /dev/null @@ -1,111 +0,0 @@ -using System; -using SkiaSharp; - -namespace Microsoft.Maui.Platform.Linux.Rendering; - -public class RenderLayer : IDisposable -{ - private SKBitmap? _bitmap; - - private SKCanvas? _canvas; - - private bool _isDirty = true; - - private SKRect _bounds; - - private bool _disposed; - - public int ZIndex { get; } - - public bool IsDirty => _isDirty; - - public bool IsVisible { get; set; } = true; - - public float Opacity { get; set; } = 1f; - - public RenderLayer(int zIndex) - { - ZIndex = zIndex; - } - - public SKCanvas BeginDraw(SKRect bounds) - { - //IL_0009: Unknown result type (might be due to invalid IL or missing references) - //IL_000e: Unknown result type (might be due to invalid IL or missing references) - //IL_0083: Unknown result type (might be due to invalid IL or missing references) - //IL_005b: Unknown result type (might be due to invalid IL or missing references) - //IL_0065: Expected O, but got Unknown - //IL_006c: Unknown result type (might be due to invalid IL or missing references) - //IL_0076: Expected O, but got Unknown - //IL_0077: Unknown result type (might be due to invalid IL or missing references) - //IL_0078: Unknown result type (might be due to invalid IL or missing references) - if (_bitmap == null || _bounds != bounds) - { - SKBitmap? bitmap = _bitmap; - if (bitmap != null) - { - ((SKNativeObject)bitmap).Dispose(); - } - SKCanvas? canvas = _canvas; - if (canvas != null) - { - ((SKNativeObject)canvas).Dispose(); - } - int num = Math.Max(1, (int)((SKRect)(ref bounds)).Width); - int num2 = Math.Max(1, (int)((SKRect)(ref bounds)).Height); - _bitmap = new SKBitmap(num, num2, (SKColorType)4, (SKAlphaType)2); - _canvas = new SKCanvas(_bitmap); - _bounds = bounds; - } - _canvas.Clear(SKColors.Transparent); - _isDirty = false; - return _canvas; - } - - public void Invalidate() - { - _isDirty = true; - } - - public void DrawTo(SKCanvas canvas, SKRect bounds) - { - //IL_0011: Unknown result type (might be due to invalid IL or missing references) - //IL_0016: Unknown result type (might be due to invalid IL or missing references) - //IL_0029: Unknown result type (might be due to invalid IL or missing references) - //IL_0034: Expected O, but got Unknown - if (!IsVisible || _bitmap == null) - { - return; - } - SKPaint val = new SKPaint - { - Color = ((SKColor)(ref SKColors.White)).WithAlpha((byte)(Opacity * 255f)) - }; - try - { - canvas.DrawBitmap(_bitmap, ((SKRect)(ref bounds)).Left, ((SKRect)(ref bounds)).Top, val); - } - finally - { - ((IDisposable)val)?.Dispose(); - } - } - - public void Dispose() - { - if (!_disposed) - { - _disposed = true; - SKCanvas? canvas = _canvas; - if (canvas != null) - { - ((SKNativeObject)canvas).Dispose(); - } - SKBitmap? bitmap = _bitmap; - if (bitmap != null) - { - ((SKNativeObject)bitmap).Dispose(); - } - } - } -} diff --git a/Rendering/ResourceCache.cs b/Rendering/ResourceCache.cs deleted file mode 100644 index 477136a..0000000 --- a/Rendering/ResourceCache.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using System.Collections.Generic; -using SkiaSharp; - -namespace Microsoft.Maui.Platform.Linux.Rendering; - -public class ResourceCache : IDisposable -{ - private readonly Dictionary _typefaces = new Dictionary(); - - private bool _disposed; - - public SKTypeface GetTypeface(string fontFamily, SKFontStyle style) - { - //IL_0052: Unknown result type (might be due to invalid IL or missing references) - string key = $"{fontFamily}_{style.Weight}_{style.Width}_{style.Slant}"; - if (!_typefaces.TryGetValue(key, out SKTypeface value)) - { - value = SKTypeface.FromFamilyName(fontFamily, style) ?? SKTypeface.Default; - _typefaces[key] = value; - } - return value; - } - - public void Clear() - { - foreach (SKTypeface value in _typefaces.Values) - { - ((SKNativeObject)value).Dispose(); - } - _typefaces.Clear(); - } - - public void Dispose() - { - if (!_disposed) - { - Clear(); - _disposed = true; - } - } -} diff --git a/Rendering/SkiaRenderingEngine.cs b/Rendering/SkiaRenderingEngine.cs index 6c1c0e8..4b4b28c 100644 --- a/Rendering/SkiaRenderingEngine.cs +++ b/Rendering/SkiaRenderingEngine.cs @@ -1,372 +1,165 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.Maui.Platform.Linux.Window; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using SkiaSharp; +using Microsoft.Maui.Platform.Linux.Window; +using Microsoft.Maui.Platform; +using System.Runtime.InteropServices; namespace Microsoft.Maui.Platform.Linux.Rendering; +/// +/// Manages Skia rendering to an X11 window. +/// public class SkiaRenderingEngine : IDisposable { - private readonly X11Window _window; + private readonly X11Window _window; + private SKBitmap? _bitmap; + private SKCanvas? _canvas; + private SKImageInfo _imageInfo; + private bool _disposed; + private bool _fullRedrawNeeded = true; - private SKBitmap? _bitmap; + public static SkiaRenderingEngine? Current { get; private set; } + public ResourceCache ResourceCache { get; } + public int Width => _imageInfo.Width; + public int Height => _imageInfo.Height; - private SKBitmap? _backBuffer; + public SkiaRenderingEngine(X11Window window) + { + _window = window; + ResourceCache = new ResourceCache(); + Current = this; - private SKCanvas? _canvas; + CreateSurface(window.Width, window.Height); - private SKImageInfo _imageInfo; + _window.Resized += OnWindowResized; + _window.Exposed += OnWindowExposed; + } - private bool _disposed; + private void CreateSurface(int width, int height) + { + _bitmap?.Dispose(); + _canvas?.Dispose(); - private bool _fullRedrawNeeded = true; + _imageInfo = new SKImageInfo( + Math.Max(1, width), + Math.Max(1, height), + SKColorType.Bgra8888, + SKAlphaType.Premul); - private readonly List _dirtyRegions = new List(); + _bitmap = new SKBitmap(_imageInfo); + _canvas = new SKCanvas(_bitmap); + _fullRedrawNeeded = true; + + } - private readonly object _dirtyLock = new object(); + private void OnWindowResized(object? sender, (int Width, int Height) size) + { + CreateSurface(size.Width, size.Height); + } - private const int MaxDirtyRegions = 32; + private void OnWindowExposed(object? sender, EventArgs e) + { + _fullRedrawNeeded = true; + } - private const float RegionMergeThreshold = 0.3f; + public void InvalidateAll() + { + _fullRedrawNeeded = true; + } - public static SkiaRenderingEngine? Current { get; private set; } + public void Render(SkiaView rootView) + { + if (_canvas == null || _bitmap == null) + return; - public ResourceCache ResourceCache { get; } + _canvas.Clear(SKColors.White); + + // Measure first, then arrange + var availableSize = new SKSize(Width, Height); + rootView.Measure(availableSize); + + rootView.Arrange(new SKRect(0, 0, Width, Height)); + + // Draw the view tree + rootView.Draw(_canvas); + + // Draw popup overlays (dropdowns, calendars, etc.) on top + SkiaView.DrawPopupOverlays(_canvas); - public int Width => ((SKImageInfo)(ref _imageInfo)).Width; + // Draw modal dialogs on top of everything + if (LinuxDialogService.HasActiveDialog) + { + LinuxDialogService.DrawDialogs(_canvas, new SKRect(0, 0, Width, Height)); + } - public int Height => ((SKImageInfo)(ref _imageInfo)).Height; + _canvas.Flush(); - public bool EnableDirtyRegionOptimization { get; set; } = true; + // Present to X11 window + PresentToWindow(); + } - public int DirtyRegionCount - { - get - { - lock (_dirtyLock) - { - return _dirtyRegions.Count; - } - } - } + private void PresentToWindow() + { + if (_bitmap == null) return; - public SkiaRenderingEngine(X11Window window) - { - _window = window; - ResourceCache = new ResourceCache(); - Current = this; - CreateSurface(window.Width, window.Height); - _window.Resized += OnWindowResized; - _window.Exposed += OnWindowExposed; - } + var pixels = _bitmap.GetPixels(); + if (pixels == IntPtr.Zero) return; - private void CreateSurface(int width, int height) - { - //IL_0044: Unknown result type (might be due to invalid IL or missing references) - //IL_0049: Unknown result type (might be due to invalid IL or missing references) - //IL_0050: Unknown result type (might be due to invalid IL or missing references) - //IL_0055: Unknown result type (might be due to invalid IL or missing references) - //IL_005f: Expected O, but got Unknown - //IL_0061: Unknown result type (might be due to invalid IL or missing references) - //IL_0066: Unknown result type (might be due to invalid IL or missing references) - //IL_0070: Expected O, but got Unknown - //IL_0077: Unknown result type (might be due to invalid IL or missing references) - //IL_0081: Expected O, but got Unknown - SKBitmap? bitmap = _bitmap; - if (bitmap != null) - { - ((SKNativeObject)bitmap).Dispose(); - } - SKBitmap? backBuffer = _backBuffer; - if (backBuffer != null) - { - ((SKNativeObject)backBuffer).Dispose(); - } - SKCanvas? canvas = _canvas; - if (canvas != null) - { - ((SKNativeObject)canvas).Dispose(); - } - _imageInfo = new SKImageInfo(Math.Max(1, width), Math.Max(1, height), (SKColorType)6, (SKAlphaType)2); - _bitmap = new SKBitmap(_imageInfo); - _backBuffer = new SKBitmap(_imageInfo); - _canvas = new SKCanvas(_bitmap); - _fullRedrawNeeded = true; - lock (_dirtyLock) - { - _dirtyRegions.Clear(); - } - } + _window.DrawPixels(pixels, _imageInfo.Width, _imageInfo.Height, _imageInfo.RowBytes); + } - private void OnWindowResized(object? sender, (int Width, int Height) size) - { - CreateSurface(size.Width, size.Height); - } + public SKCanvas? GetCanvas() => _canvas; - private void OnWindowExposed(object? sender, EventArgs e) - { - _fullRedrawNeeded = true; - } + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + _window.Resized -= OnWindowResized; + _window.Exposed -= OnWindowExposed; + _canvas?.Dispose(); + _bitmap?.Dispose(); + ResourceCache.Dispose(); + if (Current == this) Current = null; + } + _disposed = true; + } + } - public void InvalidateAll() - { - _fullRedrawNeeded = true; - } - - public void InvalidateRegion(SKRect region) - { - //IL_0026: Unknown result type (might be due to invalid IL or missing references) - //IL_003f: Unknown result type (might be due to invalid IL or missing references) - //IL_0044: Unknown result type (might be due to invalid IL or missing references) - //IL_0049: Unknown result type (might be due to invalid IL or missing references) - //IL_0094: Unknown result type (might be due to invalid IL or missing references) - //IL_0099: Unknown result type (might be due to invalid IL or missing references) - //IL_009b: Unknown result type (might be due to invalid IL or missing references) - //IL_009c: Unknown result type (might be due to invalid IL or missing references) - //IL_00d1: Unknown result type (might be due to invalid IL or missing references) - //IL_00ab: Unknown result type (might be due to invalid IL or missing references) - //IL_00ac: Unknown result type (might be due to invalid IL or missing references) - //IL_00ad: Unknown result type (might be due to invalid IL or missing references) - if (((SKRect)(ref region)).IsEmpty || ((SKRect)(ref region)).Width <= 0f || ((SKRect)(ref region)).Height <= 0f) - { - return; - } - region = SKRect.Intersect(region, new SKRect(0f, 0f, (float)Width, (float)Height)); - if (((SKRect)(ref region)).IsEmpty) - { - return; - } - lock (_dirtyLock) - { - if (_dirtyRegions.Count >= 32) - { - _fullRedrawNeeded = true; - _dirtyRegions.Clear(); - return; - } - for (int i = 0; i < _dirtyRegions.Count; i++) - { - SKRect val = _dirtyRegions[i]; - if (ShouldMergeRegions(val, region)) - { - _dirtyRegions[i] = SKRect.Union(val, region); - return; - } - } - _dirtyRegions.Add(region); - } - } - - private bool ShouldMergeRegions(SKRect a, SKRect b) - { - //IL_0000: Unknown result type (might be due to invalid IL or missing references) - //IL_0001: Unknown result type (might be due to invalid IL or missing references) - //IL_0002: Unknown result type (might be due to invalid IL or missing references) - //IL_0007: Unknown result type (might be due to invalid IL or missing references) - //IL_004e: Unknown result type (might be due to invalid IL or missing references) - SKRect val = SKRect.Intersect(a, b); - if (((SKRect)(ref val)).IsEmpty) - { - SKRect val2 = default(SKRect); - ((SKRect)(ref val2))._002Ector(((SKRect)(ref a)).Left - 4f, ((SKRect)(ref a)).Top - 4f, ((SKRect)(ref a)).Right + 4f, ((SKRect)(ref a)).Bottom + 4f); - return ((SKRect)(ref val2)).IntersectsWith(b); - } - float num = ((SKRect)(ref val)).Width * ((SKRect)(ref val)).Height; - float val3 = ((SKRect)(ref a)).Width * ((SKRect)(ref a)).Height; - float val4 = ((SKRect)(ref b)).Width * ((SKRect)(ref b)).Height; - float num2 = Math.Min(val3, val4); - return num / num2 >= 0.3f; - } - - public void Render(SkiaView rootView) - { - //IL_0027: Unknown result type (might be due to invalid IL or missing references) - //IL_0028: Unknown result type (might be due to invalid IL or missing references) - //IL_0047: Unknown result type (might be due to invalid IL or missing references) - //IL_0099: Unknown result type (might be due to invalid IL or missing references) - //IL_0100: Unknown result type (might be due to invalid IL or missing references) - //IL_0105: Unknown result type (might be due to invalid IL or missing references) - //IL_0109: Unknown result type (might be due to invalid IL or missing references) - //IL_015a: Unknown result type (might be due to invalid IL or missing references) - if (_canvas == null || _bitmap == null) - { - return; - } - SKSize availableSize = default(SKSize); - ((SKSize)(ref availableSize))._002Ector((float)Width, (float)Height); - rootView.Measure(availableSize); - rootView.Arrange(new SKRect(0f, 0f, (float)Width, (float)Height)); - bool flag = _fullRedrawNeeded || !EnableDirtyRegionOptimization; - List list; - lock (_dirtyLock) - { - if (flag) - { - list = new List - { - new SKRect(0f, 0f, (float)Width, (float)Height) - }; - _dirtyRegions.Clear(); - _fullRedrawNeeded = false; - } - else - { - if (_dirtyRegions.Count == 0) - { - return; - } - list = MergeOverlappingRegions(_dirtyRegions.ToList()); - _dirtyRegions.Clear(); - } - } - foreach (SKRect item in list) - { - RenderRegion(rootView, item, flag); - } - SkiaView.DrawPopupOverlays(_canvas); - if (LinuxDialogService.HasActiveDialog) - { - LinuxDialogService.DrawDialogs(_canvas, new SKRect(0f, 0f, (float)Width, (float)Height)); - } - _canvas.Flush(); - PresentToWindow(); - } - - private void RenderRegion(SkiaView rootView, SKRect region, bool isFullRedraw) - { - //IL_0026: Unknown result type (might be due to invalid IL or missing references) - //IL_002b: Unknown result type (might be due to invalid IL or missing references) - //IL_002c: Unknown result type (might be due to invalid IL or missing references) - //IL_0036: Unknown result type (might be due to invalid IL or missing references) - //IL_003e: Expected O, but got Unknown - //IL_001e: Unknown result type (might be due to invalid IL or missing references) - //IL_0044: Unknown result type (might be due to invalid IL or missing references) - if (_canvas == null) - { - return; - } - _canvas.Save(); - if (!isFullRedraw) - { - _canvas.ClipRect(region, (SKClipOperation)1, false); - } - SKPaint val = new SKPaint - { - Color = SKColors.White, - Style = (SKPaintStyle)0 - }; - try - { - _canvas.DrawRect(region, val); - rootView.Draw(_canvas); - _canvas.Restore(); - } - finally - { - ((IDisposable)val)?.Dispose(); - } - } - - private List MergeOverlappingRegions(List regions) - { - //IL_0028: Unknown result type (might be due to invalid IL or missing references) - //IL_002d: Unknown result type (might be due to invalid IL or missing references) - //IL_0043: Unknown result type (might be due to invalid IL or missing references) - //IL_0047: Unknown result type (might be due to invalid IL or missing references) - //IL_007f: Unknown result type (might be due to invalid IL or missing references) - //IL_0053: Unknown result type (might be due to invalid IL or missing references) - //IL_0057: Unknown result type (might be due to invalid IL or missing references) - //IL_005c: Unknown result type (might be due to invalid IL or missing references) - //IL_0061: Unknown result type (might be due to invalid IL or missing references) - if (regions.Count <= 1) - { - return regions; - } - List list = new List(); - bool[] array = new bool[regions.Count]; - for (int i = 0; i < regions.Count; i++) - { - if (array[i]) - { - continue; - } - SKRect val = regions[i]; - array[i] = true; - bool flag; - do - { - flag = false; - for (int j = i + 1; j < regions.Count; j++) - { - if (!array[j] && ShouldMergeRegions(val, regions[j])) - { - val = SKRect.Union(val, regions[j]); - array[j] = true; - flag = true; - } - } - } - while (flag); - list.Add(val); - } - return list; - } - - private void PresentToWindow() - { - if (_bitmap != null) - { - IntPtr pixels = _bitmap.GetPixels(); - if (pixels != IntPtr.Zero) - { - _window.DrawPixels(pixels, ((SKImageInfo)(ref _imageInfo)).Width, ((SKImageInfo)(ref _imageInfo)).Height, ((SKImageInfo)(ref _imageInfo)).RowBytes); - } - } - } - - public SKCanvas? GetCanvas() - { - return _canvas; - } - - protected virtual void Dispose(bool disposing) - { - if (_disposed) - { - return; - } - if (disposing) - { - _window.Resized -= OnWindowResized; - _window.Exposed -= OnWindowExposed; - SKCanvas? canvas = _canvas; - if (canvas != null) - { - ((SKNativeObject)canvas).Dispose(); - } - SKBitmap? bitmap = _bitmap; - if (bitmap != null) - { - ((SKNativeObject)bitmap).Dispose(); - } - SKBitmap? backBuffer = _backBuffer; - if (backBuffer != null) - { - ((SKNativeObject)backBuffer).Dispose(); - } - ResourceCache.Dispose(); - if (Current == this) - { - Current = null; - } - } - _disposed = true; - } - - public void Dispose() - { - Dispose(disposing: true); - GC.SuppressFinalize(this); - } + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } +} + +public class ResourceCache : IDisposable +{ + private readonly Dictionary _typefaces = new(); + private bool _disposed; + + public SKTypeface GetTypeface(string fontFamily, SKFontStyle style) + { + var key = $"{fontFamily}_{style.Weight}_{style.Width}_{style.Slant}"; + if (!_typefaces.TryGetValue(key, out var typeface)) + { + typeface = SKTypeface.FromFamilyName(fontFamily, style) ?? SKTypeface.Default; + _typefaces[key] = typeface; + } + return typeface; + } + + public void Clear() + { + foreach (var tf in _typefaces.Values) tf.Dispose(); + _typefaces.Clear(); + } + + public void Dispose() + { + if (!_disposed) { Clear(); _disposed = true; } + } } diff --git a/Rendering/TextRenderCache.cs b/Rendering/TextRenderCache.cs deleted file mode 100644 index babc14c..0000000 --- a/Rendering/TextRenderCache.cs +++ /dev/null @@ -1,142 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using SkiaSharp; - -namespace Microsoft.Maui.Platform.Linux.Rendering; - -public class TextRenderCache : IDisposable -{ - private readonly struct TextCacheKey : IEquatable - { - private readonly string _text; - - private readonly float _textSize; - - private readonly SKColor _color; - - private readonly int _weight; - - private readonly int _hashCode; - - public TextCacheKey(string text, SKPaint paint) - { - //IL_0015: Unknown result type (might be due to invalid IL or missing references) - //IL_001a: Unknown result type (might be due to invalid IL or missing references) - //IL_0049: Unknown result type (might be due to invalid IL or missing references) - _text = text; - _textSize = paint.TextSize; - _color = paint.Color; - SKTypeface typeface = paint.Typeface; - _weight = ((typeface != null) ? typeface.FontWeight : 400); - _hashCode = HashCode.Combine(_text, _textSize, _color, _weight); - } - - public bool Equals(TextCacheKey other) - { - //IL_002d: Unknown result type (might be due to invalid IL or missing references) - //IL_0033: Unknown result type (might be due to invalid IL or missing references) - if (_text == other._text && Math.Abs(_textSize - other._textSize) < 0.001f && _color == other._color) - { - return _weight == other._weight; - } - return false; - } - - public override bool Equals(object? obj) - { - if (obj is TextCacheKey other) - { - return Equals(other); - } - return false; - } - - public override int GetHashCode() - { - return _hashCode; - } - } - - private readonly Dictionary _cache = new Dictionary(); - - private readonly object _lock = new object(); - - private int _maxEntries = 500; - - private bool _disposed; - - public int MaxEntries - { - get - { - return _maxEntries; - } - set - { - _maxEntries = Math.Max(10, value); - } - } - - public SKBitmap GetOrCreate(string text, SKPaint paint) - { - //IL_0034: Unknown result type (might be due to invalid IL or missing references) - //IL_0076: Unknown result type (might be due to invalid IL or missing references) - //IL_007d: Expected O, but got Unknown - //IL_007f: Unknown result type (might be due to invalid IL or missing references) - //IL_0086: Expected O, but got Unknown - //IL_0088: Unknown result type (might be due to invalid IL or missing references) - TextCacheKey key = new TextCacheKey(text, paint); - lock (_lock) - { - if (_cache.TryGetValue(key, out SKBitmap value)) - { - return value; - } - SKRect val = default(SKRect); - paint.MeasureText(text, ref val); - int num = Math.Max(1, (int)Math.Ceiling(((SKRect)(ref val)).Width) + 2); - int num2 = Math.Max(1, (int)Math.Ceiling(((SKRect)(ref val)).Height) + 2); - SKBitmap val2 = new SKBitmap(num, num2, (SKColorType)4, (SKAlphaType)2); - SKCanvas val3 = new SKCanvas(val2); - try - { - val3.Clear(SKColors.Transparent); - val3.DrawText(text, 0f - ((SKRect)(ref val)).Left + 1f, 0f - ((SKRect)(ref val)).Top + 1f, paint); - } - finally - { - ((IDisposable)val3)?.Dispose(); - } - if (_cache.Count >= _maxEntries) - { - KeyValuePair keyValuePair = _cache.First(); - ((SKNativeObject)keyValuePair.Value).Dispose(); - _cache.Remove(keyValuePair.Key); - } - _cache[key] = val2; - return val2; - } - } - - public void Clear() - { - lock (_lock) - { - foreach (SKBitmap value in _cache.Values) - { - ((SKNativeObject)value).Dispose(); - } - _cache.Clear(); - } - } - - public void Dispose() - { - if (!_disposed) - { - _disposed = true; - Clear(); - } - } -} diff --git a/Services/-FontFallbackManager-FAC9D2911A2850E174CCA7662C668F37C2FBBA325CAF5C11AFE3FA59C16CC64ED__StringBuilder.cs b/Services/-FontFallbackManager-FAC9D2911A2850E174CCA7662C668F37C2FBBA325CAF5C11AFE3FA59C16CC64ED__StringBuilder.cs deleted file mode 100644 index 81319e3..0000000 --- a/Services/-FontFallbackManager-FAC9D2911A2850E174CCA7662C668F37C2FBBA325CAF5C11AFE3FA59C16CC64ED__StringBuilder.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Collections.Generic; - -namespace Microsoft.Maui.Platform.Linux.Services; - -internal class _003CFontFallbackManager_003EFAC9D2911A2850E174CCA7662C668F37C2FBBA325CAF5C11AFE3FA59C16CC64ED__StringBuilder -{ - private readonly List _chars = new List(); - - public int Length => _chars.Count; - - public void Append(string s) - { - _chars.AddRange(s); - } - - public void Clear() - { - _chars.Clear(); - } - - public override string ToString() - { - return new string(_chars.ToArray()); - } -} diff --git a/Services/AccessibilityServiceFactory.cs b/Services/AccessibilityServiceFactory.cs deleted file mode 100644 index 9c07b56..0000000 --- a/Services/AccessibilityServiceFactory.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System; - -namespace Microsoft.Maui.Platform.Linux.Services; - -public static class AccessibilityServiceFactory -{ - private static IAccessibilityService? _instance; - - private static readonly object _lock = new object(); - - public static IAccessibilityService Instance - { - get - { - if (_instance == null) - { - lock (_lock) - { - if (_instance == null) - { - _instance = CreateService(); - } - } - } - return _instance; - } - } - - private static IAccessibilityService CreateService() - { - try - { - AtSpi2AccessibilityService atSpi2AccessibilityService = new AtSpi2AccessibilityService(); - atSpi2AccessibilityService.Initialize(); - return atSpi2AccessibilityService; - } - catch (Exception ex) - { - Console.WriteLine("AccessibilityServiceFactory: Failed to create AT-SPI2 service - " + ex.Message); - return new NullAccessibilityService(); - } - } - - public static void Reset() - { - lock (_lock) - { - _instance?.Shutdown(); - _instance = null; - } - } -} diff --git a/Services/AccessibleAction.cs b/Services/AccessibleAction.cs deleted file mode 100644 index efcfae6..0000000 --- a/Services/AccessibleAction.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Microsoft.Maui.Platform.Linux.Services; - -public class AccessibleAction -{ - public string Name { get; set; } = string.Empty; - - public string Description { get; set; } = string.Empty; - - public string? KeyBinding { get; set; } -} diff --git a/Services/AccessibleProperty.cs b/Services/AccessibleProperty.cs deleted file mode 100644 index efaf04e..0000000 --- a/Services/AccessibleProperty.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Microsoft.Maui.Platform.Linux.Services; - -public enum AccessibleProperty -{ - Name, - Description, - Role, - Value, - Parent, - Children -} diff --git a/Services/AccessibleRect.cs b/Services/AccessibleRect.cs deleted file mode 100644 index cfb22f0..0000000 --- a/Services/AccessibleRect.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace Microsoft.Maui.Platform.Linux.Services; - -public struct AccessibleRect -{ - public int X { get; set; } - - public int Y { get; set; } - - public int Width { get; set; } - - public int Height { get; set; } - - public AccessibleRect(int x, int y, int width, int height) - { - X = x; - Y = y; - Width = width; - Height = height; - } -} diff --git a/Services/AccessibleRole.cs b/Services/AccessibleRole.cs deleted file mode 100644 index 5864431..0000000 --- a/Services/AccessibleRole.cs +++ /dev/null @@ -1,49 +0,0 @@ -namespace Microsoft.Maui.Platform.Linux.Services; - -public enum AccessibleRole -{ - Unknown, - Window, - Application, - Panel, - Frame, - Button, - CheckBox, - RadioButton, - ComboBox, - Entry, - Label, - List, - ListItem, - Menu, - MenuBar, - MenuItem, - ScrollBar, - Slider, - SpinButton, - StatusBar, - Tab, - TabPanel, - Text, - ToggleButton, - ToolBar, - ToolTip, - Tree, - TreeItem, - Image, - ProgressBar, - Separator, - Link, - Table, - TableCell, - TableRow, - TableColumnHeader, - TableRowHeader, - PageTab, - PageTabList, - Dialog, - Alert, - Filler, - Icon, - Canvas -} diff --git a/Services/AccessibleState.cs b/Services/AccessibleState.cs deleted file mode 100644 index 382ccda..0000000 --- a/Services/AccessibleState.cs +++ /dev/null @@ -1,38 +0,0 @@ -namespace Microsoft.Maui.Platform.Linux.Services; - -public enum AccessibleState -{ - Active, - Armed, - Busy, - Checked, - Collapsed, - Defunct, - Editable, - Enabled, - Expandable, - Expanded, - Focusable, - Focused, - Horizontal, - Iconified, - Modal, - MultiLine, - Opaque, - Pressed, - Resizable, - Selectable, - Selected, - Sensitive, - Showing, - SingleLine, - Stale, - Transient, - Vertical, - Visible, - ManagesDescendants, - Indeterminate, - Required, - InvalidEntry, - ReadOnly -} diff --git a/Services/AccessibleStates.cs b/Services/AccessibleStates.cs deleted file mode 100644 index 0256099..0000000 --- a/Services/AccessibleStates.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; - -namespace Microsoft.Maui.Platform.Linux.Services; - -[Flags] -public enum AccessibleStates : long -{ - None = 0L, - Active = 1L, - Armed = 2L, - Busy = 4L, - Checked = 8L, - Collapsed = 0x10L, - Defunct = 0x20L, - Editable = 0x40L, - Enabled = 0x80L, - Expandable = 0x100L, - Expanded = 0x200L, - Focusable = 0x400L, - Focused = 0x800L, - HasToolTip = 0x1000L, - Horizontal = 0x2000L, - Iconified = 0x4000L, - Modal = 0x8000L, - MultiLine = 0x10000L, - MultiSelectable = 0x20000L, - Opaque = 0x40000L, - Pressed = 0x80000L, - Resizable = 0x100000L, - Selectable = 0x200000L, - Selected = 0x400000L, - Sensitive = 0x800000L, - Showing = 0x1000000L, - SingleLine = 0x2000000L, - Stale = 0x4000000L, - Transient = 0x8000000L, - Vertical = 0x10000000L, - Visible = 0x20000000L, - ManagesDescendants = 0x40000000L, - Indeterminate = 0x80000000L, - Required = 0x100000000L, - Truncated = 0x200000000L, - Animated = 0x400000000L, - InvalidEntry = 0x800000000L, - SupportsAutocompletion = 0x1000000000L, - SelectableText = 0x2000000000L, - IsDefault = 0x4000000000L, - Visited = 0x8000000000L, - ReadOnly = 0x10000000000L -} diff --git a/Services/AnnouncementPriority.cs b/Services/AnnouncementPriority.cs deleted file mode 100644 index 60d37c1..0000000 --- a/Services/AnnouncementPriority.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Microsoft.Maui.Platform.Linux.Services; - -public enum AnnouncementPriority -{ - Polite, - Assertive -} diff --git a/Services/AppActionsService.cs b/Services/AppActionsService.cs index 0d56807..ce14843 100644 --- a/Services/AppActionsService.cs +++ b/Services/AppActionsService.cs @@ -1,149 +1,147 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using Microsoft.Maui.ApplicationModel; namespace Microsoft.Maui.Platform.Linux.Services; +/// +/// Linux app actions implementation using desktop file actions. +/// public class AppActionsService : IAppActions { - private readonly List _actions = new List(); + private readonly List _actions = new(); + private static readonly string DesktopFilesPath; - private static readonly string DesktopFilesPath; + static AppActionsService() + { + DesktopFilesPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "applications"); + } - public bool IsSupported => true; + public bool IsSupported => true; - public event EventHandler? AppActionActivated; + public event EventHandler? AppActionActivated; - static AppActionsService() - { - DesktopFilesPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "applications"); - } + public Task> GetAsync() + { + return Task.FromResult>(_actions.AsReadOnly()); + } - public Task> GetAsync() - { - return Task.FromResult((IEnumerable)_actions.AsReadOnly()); - } + public Task SetAsync(IEnumerable actions) + { + _actions.Clear(); + _actions.AddRange(actions); - public Task SetAsync(IEnumerable actions) - { - _actions.Clear(); - _actions.AddRange(actions); - UpdateDesktopActions(); - return Task.CompletedTask; - } + // On Linux, app actions can be exposed via .desktop file Actions + // This would require modifying the application's .desktop file + UpdateDesktopActions(); - private void UpdateDesktopActions() - { - } + return Task.CompletedTask; + } - public void HandleActionArgument(string actionId) - { - //IL_0035: Unknown result type (might be due to invalid IL or missing references) - //IL_003f: Expected O, but got Unknown - AppAction val = ((IEnumerable)_actions).FirstOrDefault((Func)((AppAction a) => a.Id == actionId)); - if (val != null) - { - this.AppActionActivated?.Invoke(this, new AppActionEventArgs(val)); - } - } + private void UpdateDesktopActions() + { + // Desktop actions are defined in the .desktop file + // Example: + // [Desktop Action new-window] + // Name=New Window + // Exec=myapp --action=new-window - public void CreateDesktopFile(string appName, string execPath, string? iconPath = null) - { - try - { - if (!Directory.Exists(DesktopFilesPath)) - { - Directory.CreateDirectory(DesktopFilesPath); - } - string contents = GenerateDesktopFileContent(appName, execPath, iconPath); - string path = Path.Combine(DesktopFilesPath, appName.ToLowerInvariant().Replace(" ", "-") + ".desktop"); - File.WriteAllText(path, contents); - File.SetUnixFileMode(path, UnixFileMode.OtherRead | UnixFileMode.GroupRead | UnixFileMode.UserExecute | UnixFileMode.UserWrite | UnixFileMode.UserRead); - } - catch - { - } - } + // For a proper implementation, we would need to: + // 1. Find or create the application's .desktop file + // 2. Add [Desktop Action] sections for each action + // 3. The actions would then appear in the dock/launcher right-click menu - private string GenerateDesktopFileContent(string appName, string execPath, string? iconPath) - { - StringBuilder stringBuilder = new StringBuilder(); - stringBuilder.AppendLine("[Desktop Entry]"); - stringBuilder.AppendLine("Type=Application"); - StringBuilder stringBuilder2 = stringBuilder; - StringBuilder stringBuilder3 = stringBuilder2; - StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(5, 1, stringBuilder2); - handler.AppendLiteral("Name="); - handler.AppendFormatted(appName); - stringBuilder3.AppendLine(ref handler); - stringBuilder2 = stringBuilder; - StringBuilder stringBuilder4 = stringBuilder2; - handler = new StringBuilder.AppendInterpolatedStringHandler(8, 1, stringBuilder2); - handler.AppendLiteral("Exec="); - handler.AppendFormatted(execPath); - handler.AppendLiteral(" %U"); - stringBuilder4.AppendLine(ref handler); - if (!string.IsNullOrEmpty(iconPath) && File.Exists(iconPath)) - { - stringBuilder2 = stringBuilder; - StringBuilder stringBuilder5 = stringBuilder2; - handler = new StringBuilder.AppendInterpolatedStringHandler(5, 1, stringBuilder2); - handler.AppendLiteral("Icon="); - handler.AppendFormatted(iconPath); - stringBuilder5.AppendLine(ref handler); - } - stringBuilder.AppendLine("Terminal=false"); - stringBuilder.AppendLine("Categories=Utility;"); - if (_actions.Count > 0) - { - string value = string.Join(";", _actions.Select((AppAction a) => a.Id)); - stringBuilder2 = stringBuilder; - StringBuilder stringBuilder6 = stringBuilder2; - handler = new StringBuilder.AppendInterpolatedStringHandler(9, 1, stringBuilder2); - handler.AppendLiteral("Actions="); - handler.AppendFormatted(value); - handler.AppendLiteral(";"); - stringBuilder6.AppendLine(ref handler); - stringBuilder.AppendLine(); - foreach (AppAction action in _actions) - { - stringBuilder2 = stringBuilder; - StringBuilder stringBuilder7 = stringBuilder2; - handler = new StringBuilder.AppendInterpolatedStringHandler(17, 1, stringBuilder2); - handler.AppendLiteral("[Desktop Action "); - handler.AppendFormatted(action.Id); - handler.AppendLiteral("]"); - stringBuilder7.AppendLine(ref handler); - stringBuilder2 = stringBuilder; - StringBuilder stringBuilder8 = stringBuilder2; - handler = new StringBuilder.AppendInterpolatedStringHandler(5, 1, stringBuilder2); - handler.AppendLiteral("Name="); - handler.AppendFormatted(action.Title); - stringBuilder8.AppendLine(ref handler); - if (!string.IsNullOrEmpty(action.Subtitle)) - { - stringBuilder2 = stringBuilder; - StringBuilder stringBuilder9 = stringBuilder2; - handler = new StringBuilder.AppendInterpolatedStringHandler(8, 1, stringBuilder2); - handler.AppendLiteral("Comment="); - handler.AppendFormatted(action.Subtitle); - stringBuilder9.AppendLine(ref handler); - } - stringBuilder2 = stringBuilder; - StringBuilder stringBuilder10 = stringBuilder2; - handler = new StringBuilder.AppendInterpolatedStringHandler(15, 2, stringBuilder2); - handler.AppendLiteral("Exec="); - handler.AppendFormatted(execPath); - handler.AppendLiteral(" --action="); - handler.AppendFormatted(action.Id); - stringBuilder10.AppendLine(ref handler); - stringBuilder.AppendLine(); - } - } - return stringBuilder.ToString(); - } + // This is a simplified implementation that logs actions + // A full implementation would require more system integration + } + + /// + /// Call this method to handle command-line action arguments. + /// + public void HandleActionArgument(string actionId) + { + var action = _actions.FirstOrDefault(a => a.Id == actionId); + if (action != null) + { + AppActionActivated?.Invoke(this, new AppActionEventArgs(action)); + } + } + + /// + /// Creates a .desktop file for the application with the defined actions. + /// + public void CreateDesktopFile(string appName, string execPath, string? iconPath = null) + { + try + { + if (!Directory.Exists(DesktopFilesPath)) + { + Directory.CreateDirectory(DesktopFilesPath); + } + + var desktopContent = GenerateDesktopFileContent(appName, execPath, iconPath); + var desktopFilePath = Path.Combine(DesktopFilesPath, $"{appName.ToLowerInvariant().Replace(" ", "-")}.desktop"); + + File.WriteAllText(desktopFilePath, desktopContent); + + // Make it executable + File.SetUnixFileMode(desktopFilePath, + UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute | + UnixFileMode.GroupRead | UnixFileMode.OtherRead); + } + catch + { + // Silently fail - desktop file creation is optional + } + } + + private string GenerateDesktopFileContent(string appName, string execPath, string? iconPath) + { + var content = new System.Text.StringBuilder(); + + content.AppendLine("[Desktop Entry]"); + content.AppendLine("Type=Application"); + content.AppendLine($"Name={appName}"); + content.AppendLine($"Exec={execPath} %U"); + + if (!string.IsNullOrEmpty(iconPath) && File.Exists(iconPath)) + { + content.AppendLine($"Icon={iconPath}"); + } + + content.AppendLine("Terminal=false"); + content.AppendLine("Categories=Utility;"); + + // Add actions list + if (_actions.Count > 0) + { + var actionIds = string.Join(";", _actions.Select(a => a.Id)); + content.AppendLine($"Actions={actionIds};"); + content.AppendLine(); + + // Add each action section + foreach (var action in _actions) + { + content.AppendLine($"[Desktop Action {action.Id}]"); + content.AppendLine($"Name={action.Title}"); + + if (!string.IsNullOrEmpty(action.Subtitle)) + { + content.AppendLine($"Comment={action.Subtitle}"); + } + + content.AppendLine($"Exec={execPath} --action={action.Id}"); + + { + } + + content.AppendLine(); + } + } + + return content.ToString(); + } } diff --git a/Services/AppInfoService.cs b/Services/AppInfoService.cs deleted file mode 100644 index 7acce10..0000000 --- a/Services/AppInfoService.cs +++ /dev/null @@ -1,147 +0,0 @@ -using System; -using System.Diagnostics; -using System.Reflection; -using Microsoft.Maui.ApplicationModel; - -namespace Microsoft.Maui.Platform.Linux.Services; - -public class AppInfoService : IAppInfo -{ - private static readonly Lazy _instance = new Lazy(() => new AppInfoService()); - - private readonly Assembly _entryAssembly; - - private readonly string _packageName; - - private readonly string _name; - - private readonly string _versionString; - - private readonly Version _version; - - private readonly string _buildString; - - public static AppInfoService Instance => _instance.Value; - - public string PackageName => _packageName; - - public string Name => _name; - - public string VersionString => _versionString; - - public Version Version => _version; - - public string BuildString => _buildString; - - public LayoutDirection RequestedLayoutDirection => (LayoutDirection)1; - - public AppTheme RequestedTheme - { - get - { - //IL_0042: Unknown result type (might be due to invalid IL or missing references) - //IL_0045: Unknown result type (might be due to invalid IL or missing references) - //IL_003d: Unknown result type (might be due to invalid IL or missing references) - //IL_0039: Unknown result type (might be due to invalid IL or missing references) - //IL_0022: Unknown result type (might be due to invalid IL or missing references) - try - { - string environmentVariable = Environment.GetEnvironmentVariable("GTK_THEME"); - if (!string.IsNullOrEmpty(environmentVariable) && environmentVariable.Contains("dark", StringComparison.OrdinalIgnoreCase)) - { - return (AppTheme)2; - } - if (GetGnomeColorScheme().Contains("dark", StringComparison.OrdinalIgnoreCase)) - { - return (AppTheme)2; - } - return (AppTheme)1; - } - catch - { - return (AppTheme)1; - } - } - } - - public AppPackagingModel PackagingModel - { - get - { - if (Environment.GetEnvironmentVariable("FLATPAK_ID") == null) - { - if (Environment.GetEnvironmentVariable("SNAP") == null) - { - if (Environment.GetEnvironmentVariable("APPIMAGE") == null) - { - return (AppPackagingModel)1; - } - return (AppPackagingModel)0; - } - return (AppPackagingModel)0; - } - return (AppPackagingModel)0; - } - } - - public AppInfoService() - { - _entryAssembly = Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly(); - _packageName = _entryAssembly.GetName().Name ?? "Unknown"; - _name = _entryAssembly.GetCustomAttribute()?.Title ?? _packageName; - _versionString = (_version = _entryAssembly.GetName().Version ?? new Version(1, 0)).ToString(); - _buildString = _entryAssembly.GetCustomAttribute()?.InformationalVersion ?? _versionString; - } - - private string GetGnomeColorScheme() - { - try - { - using Process process = Process.Start(new ProcessStartInfo - { - FileName = "gsettings", - Arguments = "get org.gnome.desktop.interface color-scheme", - UseShellExecute = false, - RedirectStandardOutput = true, - CreateNoWindow = true - }); - if (process != null) - { - string text = process.StandardOutput.ReadToEnd(); - process.WaitForExit(1000); - return text.Trim().Trim('\''); - } - } - catch - { - } - return ""; - } - - public void ShowSettingsUI() - { - try - { - Process.Start(new ProcessStartInfo - { - FileName = "gnome-control-center", - UseShellExecute = true - }); - } - catch - { - try - { - Process.Start(new ProcessStartInfo - { - FileName = "xdg-open", - Arguments = "x-settings:", - UseShellExecute = true - }); - } - catch - { - } - } - } -} diff --git a/Services/AtSpi2AccessibilityService.cs b/Services/AtSpi2AccessibilityService.cs index a977787..d3d7f00 100644 --- a/Services/AtSpi2AccessibilityService.cs +++ b/Services/AtSpi2AccessibilityService.cs @@ -1,500 +1,461 @@ -using System; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using System.Collections.Concurrent; using System.Runtime.InteropServices; namespace Microsoft.Maui.Platform.Linux.Services; +/// +/// AT-SPI2 accessibility service implementation. +/// Provides screen reader support through the AT-SPI2 D-Bus interface. +/// public class AtSpi2AccessibilityService : IAccessibilityService, IDisposable { - private IntPtr _connection; - - private IntPtr _registry; - - private bool _isEnabled; - - private bool _disposed; - - private IAccessible? _focusedAccessible; - - private readonly ConcurrentDictionary _registeredObjects = new ConcurrentDictionary(); - - private readonly string _applicationName; - - private IntPtr _applicationAccessible; - - private const int ATSPI_ROLE_UNKNOWN = 0; - - private const int ATSPI_ROLE_WINDOW = 22; - - private const int ATSPI_ROLE_APPLICATION = 75; - - private const int ATSPI_ROLE_PANEL = 25; - - private const int ATSPI_ROLE_FRAME = 11; - - private const int ATSPI_ROLE_PUSH_BUTTON = 31; - - private const int ATSPI_ROLE_CHECK_BOX = 4; - - private const int ATSPI_ROLE_RADIO_BUTTON = 33; - - private const int ATSPI_ROLE_COMBO_BOX = 6; - - private const int ATSPI_ROLE_ENTRY = 24; - - private const int ATSPI_ROLE_LABEL = 16; - - private const int ATSPI_ROLE_LIST = 17; - - private const int ATSPI_ROLE_LIST_ITEM = 18; - - private const int ATSPI_ROLE_MENU = 19; - - private const int ATSPI_ROLE_MENU_BAR = 20; - - private const int ATSPI_ROLE_MENU_ITEM = 21; - - private const int ATSPI_ROLE_SCROLL_BAR = 40; - - private const int ATSPI_ROLE_SLIDER = 43; - - private const int ATSPI_ROLE_SPIN_BUTTON = 44; - - private const int ATSPI_ROLE_STATUS_BAR = 46; - - private const int ATSPI_ROLE_PAGE_TAB = 26; - - private const int ATSPI_ROLE_PAGE_TAB_LIST = 27; - - private const int ATSPI_ROLE_TEXT = 49; - - private const int ATSPI_ROLE_TOGGLE_BUTTON = 51; - - private const int ATSPI_ROLE_TOOL_BAR = 52; - - private const int ATSPI_ROLE_TOOL_TIP = 53; - - private const int ATSPI_ROLE_TREE = 54; - - private const int ATSPI_ROLE_TREE_ITEM = 55; - - private const int ATSPI_ROLE_IMAGE = 14; - - private const int ATSPI_ROLE_PROGRESS_BAR = 30; - - private const int ATSPI_ROLE_SEPARATOR = 42; - - private const int ATSPI_ROLE_LINK = 83; - - private const int ATSPI_ROLE_TABLE = 47; - - private const int ATSPI_ROLE_TABLE_CELL = 48; - - private const int ATSPI_ROLE_TABLE_ROW = 89; - - private const int ATSPI_ROLE_TABLE_COLUMN_HEADER = 36; - - private const int ATSPI_ROLE_TABLE_ROW_HEADER = 37; - - private const int ATSPI_ROLE_DIALOG = 8; - - private const int ATSPI_ROLE_ALERT = 2; - - private const int ATSPI_ROLE_FILLER = 10; - - private const int ATSPI_ROLE_ICON = 13; - - private const int ATSPI_ROLE_CANVAS = 3; - - public bool IsEnabled => _isEnabled; - - public AtSpi2AccessibilityService(string applicationName = "MAUI Application") - { - _applicationName = applicationName; - } - - public void Initialize() - { - try - { - if (atspi_init() != 0) - { - Console.WriteLine("AtSpi2AccessibilityService: Failed to initialize AT-SPI2"); - return; - } - _isEnabled = CheckAccessibilityEnabled(); - if (_isEnabled) - { - _registry = atspi_get_desktop(0); - RegisterApplication(); - Console.WriteLine("AtSpi2AccessibilityService: Initialized successfully"); - } - else - { - Console.WriteLine("AtSpi2AccessibilityService: Accessibility is not enabled"); - } - } - catch (Exception ex) - { - Console.WriteLine("AtSpi2AccessibilityService: Initialization failed - " + ex.Message); - } - } - - private bool CheckAccessibilityEnabled() - { - try - { - IntPtr intPtr = atspi_get_desktop(0); - if (intPtr != IntPtr.Zero) - { - g_object_unref(intPtr); - return true; - } - } - catch - { - } - return Environment.GetEnvironmentVariable("GTK_A11Y")?.ToLowerInvariant() != "none"; - } - - private void RegisterApplication() - { - atspi_set_main_context(IntPtr.Zero); - } - - public void Register(IAccessible accessible) - { - if (accessible != null) - { - _registeredObjects.TryAdd(accessible.AccessibleId, accessible); - } - } - - public void Unregister(IAccessible accessible) - { - if (accessible != null) - { - _registeredObjects.TryRemove(accessible.AccessibleId, out IAccessible _); - } - } - - public void NotifyFocusChanged(IAccessible? accessible) - { - _focusedAccessible = accessible; - if (_isEnabled && accessible != null) - { - EmitEvent("focus:", accessible); - } - } - - public void NotifyPropertyChanged(IAccessible accessible, AccessibleProperty property) - { - if (_isEnabled && accessible != null) - { - string text = property switch - { - AccessibleProperty.Name => "object:property-change:accessible-name", - AccessibleProperty.Description => "object:property-change:accessible-description", - AccessibleProperty.Role => "object:property-change:accessible-role", - AccessibleProperty.Value => "object:property-change:accessible-value", - AccessibleProperty.Parent => "object:property-change:accessible-parent", - AccessibleProperty.Children => "object:children-changed", - _ => string.Empty, - }; - if (!string.IsNullOrEmpty(text)) - { - EmitEvent(text, accessible); - } - } - } - - public void NotifyStateChanged(IAccessible accessible, AccessibleState state, bool value) - { - if (_isEnabled && accessible != null) - { - string text = state.ToString().ToLowerInvariant(); - string eventName = "object:state-changed:" + text; - EmitEvent(eventName, accessible, value ? 1 : 0); - } - } - - public void Announce(string text, AnnouncementPriority priority = AnnouncementPriority.Polite) - { - if (!_isEnabled || string.IsNullOrEmpty(text)) - { - return; - } - try - { - Console.WriteLine($"[Accessibility Announcement ({priority})]: {text}"); - } - catch (Exception ex) - { - Console.WriteLine("AtSpi2AccessibilityService: Announcement failed - " + ex.Message); - } - } - - private void EmitEvent(string eventName, IAccessible accessible, int detail1 = 0, int detail2 = 0) - { - Console.WriteLine($"[AT-SPI2 Event] {eventName}: {accessible.AccessibleName} ({accessible.Role})"); - } - - public static int GetAtSpiRole(AccessibleRole role) - { - return role switch - { - AccessibleRole.Unknown => 0, - AccessibleRole.Window => 22, - AccessibleRole.Application => 75, - AccessibleRole.Panel => 25, - AccessibleRole.Frame => 11, - AccessibleRole.Button => 31, - AccessibleRole.CheckBox => 4, - AccessibleRole.RadioButton => 33, - AccessibleRole.ComboBox => 6, - AccessibleRole.Entry => 24, - AccessibleRole.Label => 16, - AccessibleRole.List => 17, - AccessibleRole.ListItem => 18, - AccessibleRole.Menu => 19, - AccessibleRole.MenuBar => 20, - AccessibleRole.MenuItem => 21, - AccessibleRole.ScrollBar => 40, - AccessibleRole.Slider => 43, - AccessibleRole.SpinButton => 44, - AccessibleRole.StatusBar => 46, - AccessibleRole.Tab => 26, - AccessibleRole.TabPanel => 27, - AccessibleRole.Text => 49, - AccessibleRole.ToggleButton => 51, - AccessibleRole.ToolBar => 52, - AccessibleRole.ToolTip => 53, - AccessibleRole.Tree => 54, - AccessibleRole.TreeItem => 55, - AccessibleRole.Image => 14, - AccessibleRole.ProgressBar => 30, - AccessibleRole.Separator => 42, - AccessibleRole.Link => 83, - AccessibleRole.Table => 47, - AccessibleRole.TableCell => 48, - AccessibleRole.TableRow => 89, - AccessibleRole.TableColumnHeader => 36, - AccessibleRole.TableRowHeader => 37, - AccessibleRole.PageTab => 26, - AccessibleRole.PageTabList => 27, - AccessibleRole.Dialog => 8, - AccessibleRole.Alert => 2, - AccessibleRole.Filler => 10, - AccessibleRole.Icon => 13, - AccessibleRole.Canvas => 3, - _ => 0, - }; - } - - public static (uint Low, uint High) GetAtSpiStates(AccessibleStates states) - { - uint num = 0u; - uint num2 = 0u; - if (states.HasFlag(AccessibleStates.Active)) - { - num |= 1; - } - if (states.HasFlag(AccessibleStates.Armed)) - { - num |= 2; - } - if (states.HasFlag(AccessibleStates.Busy)) - { - num |= 4; - } - if (states.HasFlag(AccessibleStates.Checked)) - { - num |= 8; - } - if (states.HasFlag(AccessibleStates.Collapsed)) - { - num |= 0x10; - } - if (states.HasFlag(AccessibleStates.Defunct)) - { - num |= 0x20; - } - if (states.HasFlag(AccessibleStates.Editable)) - { - num |= 0x40; - } - if (states.HasFlag(AccessibleStates.Enabled)) - { - num |= 0x80; - } - if (states.HasFlag(AccessibleStates.Expandable)) - { - num |= 0x100; - } - if (states.HasFlag(AccessibleStates.Expanded)) - { - num |= 0x200; - } - if (states.HasFlag(AccessibleStates.Focusable)) - { - num |= 0x400; - } - if (states.HasFlag(AccessibleStates.Focused)) - { - num |= 0x800; - } - if (states.HasFlag(AccessibleStates.Horizontal)) - { - num |= 0x2000; - } - if (states.HasFlag(AccessibleStates.Iconified)) - { - num |= 0x4000; - } - if (states.HasFlag(AccessibleStates.Modal)) - { - num |= 0x8000; - } - if (states.HasFlag(AccessibleStates.MultiLine)) - { - num |= 0x10000; - } - if (states.HasFlag(AccessibleStates.MultiSelectable)) - { - num |= 0x20000; - } - if (states.HasFlag(AccessibleStates.Opaque)) - { - num |= 0x40000; - } - if (states.HasFlag(AccessibleStates.Pressed)) - { - num |= 0x80000; - } - if (states.HasFlag(AccessibleStates.Resizable)) - { - num |= 0x100000; - } - if (states.HasFlag(AccessibleStates.Selectable)) - { - num |= 0x200000; - } - if (states.HasFlag(AccessibleStates.Selected)) - { - num |= 0x400000; - } - if (states.HasFlag(AccessibleStates.Sensitive)) - { - num |= 0x800000; - } - if (states.HasFlag(AccessibleStates.Showing)) - { - num |= 0x1000000; - } - if (states.HasFlag(AccessibleStates.SingleLine)) - { - num |= 0x2000000; - } - if (states.HasFlag(AccessibleStates.Stale)) - { - num |= 0x4000000; - } - if (states.HasFlag(AccessibleStates.Transient)) - { - num |= 0x8000000; - } - if (states.HasFlag(AccessibleStates.Vertical)) - { - num |= 0x10000000; - } - if (states.HasFlag(AccessibleStates.Visible)) - { - num |= 0x20000000; - } - if (states.HasFlag(AccessibleStates.ManagesDescendants)) - { - num |= 0x40000000; - } - if (states.HasFlag(AccessibleStates.Indeterminate)) - { - num |= 0x80000000u; - } - if (states.HasFlag(AccessibleStates.Required)) - { - num2 |= 1; - } - if (states.HasFlag(AccessibleStates.Truncated)) - { - num2 |= 2; - } - if (states.HasFlag(AccessibleStates.Animated)) - { - num2 |= 4; - } - if (states.HasFlag(AccessibleStates.InvalidEntry)) - { - num2 |= 8; - } - if (states.HasFlag(AccessibleStates.SupportsAutocompletion)) - { - num2 |= 0x10; - } - if (states.HasFlag(AccessibleStates.SelectableText)) - { - num2 |= 0x20; - } - if (states.HasFlag(AccessibleStates.IsDefault)) - { - num2 |= 0x40; - } - if (states.HasFlag(AccessibleStates.Visited)) - { - num2 |= 0x80; - } - if (states.HasFlag(AccessibleStates.ReadOnly)) - { - num2 |= 0x400; - } - return (Low: num, High: num2); - } - - public void Shutdown() - { - Dispose(); - } - - public void Dispose() - { - if (!_disposed) - { - _disposed = true; - _registeredObjects.Clear(); - if (_applicationAccessible != IntPtr.Zero) - { - g_object_unref(_applicationAccessible); - _applicationAccessible = IntPtr.Zero; - } - if (_registry != IntPtr.Zero) - { - g_object_unref(_registry); - _registry = IntPtr.Zero; - } - atspi_exit(); - } - } - - [DllImport("libatspi.so.0")] - private static extern int atspi_init(); - - [DllImport("libatspi.so.0")] - private static extern int atspi_exit(); - - [DllImport("libatspi.so.0")] - private static extern IntPtr atspi_get_desktop(int i); - - [DllImport("libatspi.so.0")] - private static extern void atspi_set_main_context(IntPtr context); - - [DllImport("libgobject-2.0.so.0")] - private static extern void g_object_unref(IntPtr obj); + private nint _connection; + private nint _registry; + private bool _isEnabled; + private bool _disposed; + private IAccessible? _focusedAccessible; + private readonly ConcurrentDictionary _registeredObjects = new(); + private readonly string _applicationName; + private nint _applicationAccessible; + + public bool IsEnabled => _isEnabled; + + public AtSpi2AccessibilityService(string applicationName = "MAUI Application") + { + _applicationName = applicationName; + } + + public void Initialize() + { + try + { + // Initialize AT-SPI2 + int result = atspi_init(); + if (result != 0) + { + Console.WriteLine("AtSpi2AccessibilityService: Failed to initialize AT-SPI2"); + return; + } + + // Check if accessibility is enabled + _isEnabled = CheckAccessibilityEnabled(); + + if (_isEnabled) + { + // Get the desktop (root accessible) + _registry = atspi_get_desktop(0); + + // Register our application + RegisterApplication(); + + Console.WriteLine("AtSpi2AccessibilityService: Initialized successfully"); + } + else + { + Console.WriteLine("AtSpi2AccessibilityService: Accessibility is not enabled"); + } + } + catch (Exception ex) + { + Console.WriteLine($"AtSpi2AccessibilityService: Initialization failed - {ex.Message}"); + } + } + + private bool CheckAccessibilityEnabled() + { + // Check if AT-SPI2 registry is available + try + { + nint desktop = atspi_get_desktop(0); + if (desktop != IntPtr.Zero) + { + g_object_unref(desktop); + return true; + } + } + catch + { + // AT-SPI2 not available + } + + // Also check the gsettings key + var enabled = Environment.GetEnvironmentVariable("GTK_A11Y"); + return enabled?.ToLowerInvariant() != "none"; + } + + private void RegisterApplication() + { + // In a full implementation, we would create an AtspiApplication object + // and register it with the AT-SPI2 registry. For now, we set up the basics. + + // Set application name + atspi_set_main_context(IntPtr.Zero); + } + + public void Register(IAccessible accessible) + { + if (accessible == null) return; + + _registeredObjects.TryAdd(accessible.AccessibleId, accessible); + + // In a full implementation, we would create an AtspiAccessible object + // and register it with AT-SPI2 + } + + public void Unregister(IAccessible accessible) + { + if (accessible == null) return; + + _registeredObjects.TryRemove(accessible.AccessibleId, out _); + + // Clean up AT-SPI2 resources for this accessible + } + + public void NotifyFocusChanged(IAccessible? accessible) + { + _focusedAccessible = accessible; + + if (!_isEnabled || accessible == null) return; + + // Emit focus event through AT-SPI2 + EmitEvent("focus:", accessible); + } + + public void NotifyPropertyChanged(IAccessible accessible, AccessibleProperty property) + { + if (!_isEnabled || accessible == null) return; + + string eventName = property switch + { + AccessibleProperty.Name => "object:property-change:accessible-name", + AccessibleProperty.Description => "object:property-change:accessible-description", + AccessibleProperty.Role => "object:property-change:accessible-role", + AccessibleProperty.Value => "object:property-change:accessible-value", + AccessibleProperty.Parent => "object:property-change:accessible-parent", + AccessibleProperty.Children => "object:children-changed", + _ => string.Empty + }; + + if (!string.IsNullOrEmpty(eventName)) + { + EmitEvent(eventName, accessible); + } + } + + public void NotifyStateChanged(IAccessible accessible, AccessibleState state, bool value) + { + if (!_isEnabled || accessible == null) return; + + string stateName = state.ToString().ToLowerInvariant(); + string eventName = $"object:state-changed:{stateName}"; + + EmitEvent(eventName, accessible, value ? 1 : 0); + } + + public void Announce(string text, AnnouncementPriority priority = AnnouncementPriority.Polite) + { + if (!_isEnabled || string.IsNullOrEmpty(text)) return; + + // Use AT-SPI2 live region to announce text + // Priority maps to: Polite = ATSPI_LIVE_POLITE, Assertive = ATSPI_LIVE_ASSERTIVE + + try + { + // In AT-SPI2, announcements are typically done through live regions + // or by emitting "object:announcement" events + + // For now, use a simpler approach with the event system + Console.WriteLine($"[Accessibility Announcement ({priority})]: {text}"); + } + catch (Exception ex) + { + Console.WriteLine($"AtSpi2AccessibilityService: Announcement failed - {ex.Message}"); + } + } + + private void EmitEvent(string eventName, IAccessible accessible, int detail1 = 0, int detail2 = 0) + { + // In a full implementation, we would emit the event through D-Bus + // using the org.a11y.atspi.Event interface + + // For now, log the event for debugging + Console.WriteLine($"[AT-SPI2 Event] {eventName}: {accessible.AccessibleName} ({accessible.Role})"); + } + + /// + /// Gets the AT-SPI2 role value for the given accessible role. + /// + public static int GetAtSpiRole(AccessibleRole role) + { + return role switch + { + AccessibleRole.Unknown => ATSPI_ROLE_UNKNOWN, + AccessibleRole.Window => ATSPI_ROLE_WINDOW, + AccessibleRole.Application => ATSPI_ROLE_APPLICATION, + AccessibleRole.Panel => ATSPI_ROLE_PANEL, + AccessibleRole.Frame => ATSPI_ROLE_FRAME, + AccessibleRole.Button => ATSPI_ROLE_PUSH_BUTTON, + AccessibleRole.CheckBox => ATSPI_ROLE_CHECK_BOX, + AccessibleRole.RadioButton => ATSPI_ROLE_RADIO_BUTTON, + AccessibleRole.ComboBox => ATSPI_ROLE_COMBO_BOX, + AccessibleRole.Entry => ATSPI_ROLE_ENTRY, + AccessibleRole.Label => ATSPI_ROLE_LABEL, + AccessibleRole.List => ATSPI_ROLE_LIST, + AccessibleRole.ListItem => ATSPI_ROLE_LIST_ITEM, + AccessibleRole.Menu => ATSPI_ROLE_MENU, + AccessibleRole.MenuBar => ATSPI_ROLE_MENU_BAR, + AccessibleRole.MenuItem => ATSPI_ROLE_MENU_ITEM, + AccessibleRole.ScrollBar => ATSPI_ROLE_SCROLL_BAR, + AccessibleRole.Slider => ATSPI_ROLE_SLIDER, + AccessibleRole.SpinButton => ATSPI_ROLE_SPIN_BUTTON, + AccessibleRole.StatusBar => ATSPI_ROLE_STATUS_BAR, + AccessibleRole.Tab => ATSPI_ROLE_PAGE_TAB, + AccessibleRole.TabPanel => ATSPI_ROLE_PAGE_TAB_LIST, + AccessibleRole.Text => ATSPI_ROLE_TEXT, + AccessibleRole.ToggleButton => ATSPI_ROLE_TOGGLE_BUTTON, + AccessibleRole.ToolBar => ATSPI_ROLE_TOOL_BAR, + AccessibleRole.ToolTip => ATSPI_ROLE_TOOL_TIP, + AccessibleRole.Tree => ATSPI_ROLE_TREE, + AccessibleRole.TreeItem => ATSPI_ROLE_TREE_ITEM, + AccessibleRole.Image => ATSPI_ROLE_IMAGE, + AccessibleRole.ProgressBar => ATSPI_ROLE_PROGRESS_BAR, + AccessibleRole.Separator => ATSPI_ROLE_SEPARATOR, + AccessibleRole.Link => ATSPI_ROLE_LINK, + AccessibleRole.Table => ATSPI_ROLE_TABLE, + AccessibleRole.TableCell => ATSPI_ROLE_TABLE_CELL, + AccessibleRole.TableRow => ATSPI_ROLE_TABLE_ROW, + AccessibleRole.TableColumnHeader => ATSPI_ROLE_TABLE_COLUMN_HEADER, + AccessibleRole.TableRowHeader => ATSPI_ROLE_TABLE_ROW_HEADER, + AccessibleRole.PageTab => ATSPI_ROLE_PAGE_TAB, + AccessibleRole.PageTabList => ATSPI_ROLE_PAGE_TAB_LIST, + AccessibleRole.Dialog => ATSPI_ROLE_DIALOG, + AccessibleRole.Alert => ATSPI_ROLE_ALERT, + AccessibleRole.Filler => ATSPI_ROLE_FILLER, + AccessibleRole.Icon => ATSPI_ROLE_ICON, + AccessibleRole.Canvas => ATSPI_ROLE_CANVAS, + _ => ATSPI_ROLE_UNKNOWN + }; + } + + /// + /// Converts accessible states to AT-SPI2 state set. + /// + public static (uint Low, uint High) GetAtSpiStates(AccessibleStates states) + { + uint low = 0; + uint high = 0; + + if (states.HasFlag(AccessibleStates.Active)) low |= 1 << 0; + if (states.HasFlag(AccessibleStates.Armed)) low |= 1 << 1; + if (states.HasFlag(AccessibleStates.Busy)) low |= 1 << 2; + if (states.HasFlag(AccessibleStates.Checked)) low |= 1 << 3; + if (states.HasFlag(AccessibleStates.Collapsed)) low |= 1 << 4; + if (states.HasFlag(AccessibleStates.Defunct)) low |= 1 << 5; + if (states.HasFlag(AccessibleStates.Editable)) low |= 1 << 6; + if (states.HasFlag(AccessibleStates.Enabled)) low |= 1 << 7; + if (states.HasFlag(AccessibleStates.Expandable)) low |= 1 << 8; + if (states.HasFlag(AccessibleStates.Expanded)) low |= 1 << 9; + if (states.HasFlag(AccessibleStates.Focusable)) low |= 1 << 10; + if (states.HasFlag(AccessibleStates.Focused)) low |= 1 << 11; + if (states.HasFlag(AccessibleStates.Horizontal)) low |= 1 << 13; + if (states.HasFlag(AccessibleStates.Iconified)) low |= 1 << 14; + if (states.HasFlag(AccessibleStates.Modal)) low |= 1 << 15; + if (states.HasFlag(AccessibleStates.MultiLine)) low |= 1 << 16; + if (states.HasFlag(AccessibleStates.MultiSelectable)) low |= 1 << 17; + if (states.HasFlag(AccessibleStates.Opaque)) low |= 1 << 18; + if (states.HasFlag(AccessibleStates.Pressed)) low |= 1 << 19; + if (states.HasFlag(AccessibleStates.Resizable)) low |= 1 << 20; + if (states.HasFlag(AccessibleStates.Selectable)) low |= 1 << 21; + if (states.HasFlag(AccessibleStates.Selected)) low |= 1 << 22; + if (states.HasFlag(AccessibleStates.Sensitive)) low |= 1 << 23; + if (states.HasFlag(AccessibleStates.Showing)) low |= 1 << 24; + if (states.HasFlag(AccessibleStates.SingleLine)) low |= 1 << 25; + if (states.HasFlag(AccessibleStates.Stale)) low |= 1 << 26; + if (states.HasFlag(AccessibleStates.Transient)) low |= 1 << 27; + if (states.HasFlag(AccessibleStates.Vertical)) low |= 1 << 28; + if (states.HasFlag(AccessibleStates.Visible)) low |= 1 << 29; + if (states.HasFlag(AccessibleStates.ManagesDescendants)) low |= 1 << 30; + if (states.HasFlag(AccessibleStates.Indeterminate)) low |= 1u << 31; + + // High bits (states 32+) + if (states.HasFlag(AccessibleStates.Required)) high |= 1 << 0; + if (states.HasFlag(AccessibleStates.Truncated)) high |= 1 << 1; + if (states.HasFlag(AccessibleStates.Animated)) high |= 1 << 2; + if (states.HasFlag(AccessibleStates.InvalidEntry)) high |= 1 << 3; + if (states.HasFlag(AccessibleStates.SupportsAutocompletion)) high |= 1 << 4; + if (states.HasFlag(AccessibleStates.SelectableText)) high |= 1 << 5; + if (states.HasFlag(AccessibleStates.IsDefault)) high |= 1 << 6; + if (states.HasFlag(AccessibleStates.Visited)) high |= 1 << 7; + if (states.HasFlag(AccessibleStates.ReadOnly)) high |= 1 << 10; + + return (low, high); + } + + public void Shutdown() + { + Dispose(); + } + + public void Dispose() + { + if (_disposed) return; + _disposed = true; + + _registeredObjects.Clear(); + + if (_applicationAccessible != IntPtr.Zero) + { + g_object_unref(_applicationAccessible); + _applicationAccessible = IntPtr.Zero; + } + + if (_registry != IntPtr.Zero) + { + g_object_unref(_registry); + _registry = IntPtr.Zero; + } + + // Exit AT-SPI2 + atspi_exit(); + } + + #region AT-SPI2 Role Constants + + private const int ATSPI_ROLE_UNKNOWN = 0; + private const int ATSPI_ROLE_WINDOW = 22; + private const int ATSPI_ROLE_APPLICATION = 75; + private const int ATSPI_ROLE_PANEL = 25; + private const int ATSPI_ROLE_FRAME = 11; + private const int ATSPI_ROLE_PUSH_BUTTON = 31; + private const int ATSPI_ROLE_CHECK_BOX = 4; + private const int ATSPI_ROLE_RADIO_BUTTON = 33; + private const int ATSPI_ROLE_COMBO_BOX = 6; + private const int ATSPI_ROLE_ENTRY = 24; + private const int ATSPI_ROLE_LABEL = 16; + private const int ATSPI_ROLE_LIST = 17; + private const int ATSPI_ROLE_LIST_ITEM = 18; + private const int ATSPI_ROLE_MENU = 19; + private const int ATSPI_ROLE_MENU_BAR = 20; + private const int ATSPI_ROLE_MENU_ITEM = 21; + private const int ATSPI_ROLE_SCROLL_BAR = 40; + private const int ATSPI_ROLE_SLIDER = 43; + private const int ATSPI_ROLE_SPIN_BUTTON = 44; + private const int ATSPI_ROLE_STATUS_BAR = 46; + private const int ATSPI_ROLE_PAGE_TAB = 26; + private const int ATSPI_ROLE_PAGE_TAB_LIST = 27; + private const int ATSPI_ROLE_TEXT = 49; + private const int ATSPI_ROLE_TOGGLE_BUTTON = 51; + private const int ATSPI_ROLE_TOOL_BAR = 52; + private const int ATSPI_ROLE_TOOL_TIP = 53; + private const int ATSPI_ROLE_TREE = 54; + private const int ATSPI_ROLE_TREE_ITEM = 55; + private const int ATSPI_ROLE_IMAGE = 14; + private const int ATSPI_ROLE_PROGRESS_BAR = 30; + private const int ATSPI_ROLE_SEPARATOR = 42; + private const int ATSPI_ROLE_LINK = 83; + private const int ATSPI_ROLE_TABLE = 47; + private const int ATSPI_ROLE_TABLE_CELL = 48; + private const int ATSPI_ROLE_TABLE_ROW = 89; + private const int ATSPI_ROLE_TABLE_COLUMN_HEADER = 36; + private const int ATSPI_ROLE_TABLE_ROW_HEADER = 37; + private const int ATSPI_ROLE_DIALOG = 8; + private const int ATSPI_ROLE_ALERT = 2; + private const int ATSPI_ROLE_FILLER = 10; + private const int ATSPI_ROLE_ICON = 13; + private const int ATSPI_ROLE_CANVAS = 3; + + #endregion + + #region AT-SPI2 Interop + + [DllImport("libatspi.so.0")] + private static extern int atspi_init(); + + [DllImport("libatspi.so.0")] + private static extern int atspi_exit(); + + [DllImport("libatspi.so.0")] + private static extern nint atspi_get_desktop(int i); + + [DllImport("libatspi.so.0")] + private static extern void atspi_set_main_context(nint context); + + [DllImport("libgobject-2.0.so.0")] + private static extern void g_object_unref(nint obj); + + #endregion +} + +/// +/// Factory for creating accessibility service instances. +/// +public static class AccessibilityServiceFactory +{ + private static IAccessibilityService? _instance; + private static readonly object _lock = new(); + + /// + /// Gets the singleton accessibility service instance. + /// + public static IAccessibilityService Instance + { + get + { + if (_instance == null) + { + lock (_lock) + { + _instance ??= CreateService(); + } + } + return _instance; + } + } + + private static IAccessibilityService CreateService() + { + try + { + var service = new AtSpi2AccessibilityService(); + service.Initialize(); + return service; + } + catch (Exception ex) + { + Console.WriteLine($"AccessibilityServiceFactory: Failed to create AT-SPI2 service - {ex.Message}"); + return new NullAccessibilityService(); + } + } + + /// + /// Resets the singleton instance. + /// + public static void Reset() + { + lock (_lock) + { + _instance?.Shutdown(); + _instance = null; + } + } +} + +/// +/// Null implementation of accessibility service. +/// +public class NullAccessibilityService : IAccessibilityService +{ + public bool IsEnabled => false; + + public void Initialize() { } + public void Register(IAccessible accessible) { } + public void Unregister(IAccessible accessible) { } + public void NotifyFocusChanged(IAccessible? accessible) { } + public void NotifyPropertyChanged(IAccessible accessible, AccessibleProperty property) { } + public void NotifyStateChanged(IAccessible accessible, AccessibleState state, bool value) { } + public void Announce(string text, AnnouncementPriority priority = AnnouncementPriority.Polite) { } + public void Shutdown() { } } diff --git a/Services/BrowserService.cs b/Services/BrowserService.cs index 9f6bc87..b1847ac 100644 --- a/Services/BrowserService.cs +++ b/Services/BrowserService.cs @@ -1,68 +1,66 @@ -using System; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using System.Diagnostics; -using System.Threading.Tasks; using Microsoft.Maui.ApplicationModel; namespace Microsoft.Maui.Platform.Linux.Services; +/// +/// Linux browser implementation using xdg-open. +/// public class BrowserService : IBrowser { - public async Task OpenAsync(string uri) - { - return await OpenAsync(new Uri(uri), (BrowserLaunchMode)0); - } + public async Task OpenAsync(string uri) + { + return await OpenAsync(new Uri(uri), BrowserLaunchMode.SystemPreferred); + } - public async Task OpenAsync(string uri, BrowserLaunchMode launchMode) - { - //IL_001e: Unknown result type (might be due to invalid IL or missing references) - //IL_001f: Unknown result type (might be due to invalid IL or missing references) - return await OpenAsync(new Uri(uri), launchMode); - } + public async Task OpenAsync(string uri, BrowserLaunchMode launchMode) + { + return await OpenAsync(new Uri(uri), launchMode); + } - public async Task OpenAsync(Uri uri) - { - return await OpenAsync(uri, (BrowserLaunchMode)0); - } + public async Task OpenAsync(Uri uri) + { + return await OpenAsync(uri, BrowserLaunchMode.SystemPreferred); + } - public async Task OpenAsync(Uri uri, BrowserLaunchMode launchMode) - { - //IL_001e: Unknown result type (might be due to invalid IL or missing references) - //IL_001f: Unknown result type (might be due to invalid IL or missing references) - return await this.OpenAsync(uri, new BrowserLaunchOptions - { - LaunchMode = launchMode - }); - } + public async Task OpenAsync(Uri uri, BrowserLaunchMode launchMode) + { + return await OpenAsync(uri, new BrowserLaunchOptions { LaunchMode = launchMode }); + } - public async Task OpenAsync(Uri uri, BrowserLaunchOptions options) - { - if (uri == null) - { - throw new ArgumentNullException("uri"); - } - try - { - string absoluteUri = uri.AbsoluteUri; - ProcessStartInfo startInfo = new ProcessStartInfo - { - FileName = "xdg-open", - Arguments = "\"" + absoluteUri + "\"", - UseShellExecute = false, - RedirectStandardOutput = true, - RedirectStandardError = true, - CreateNoWindow = true - }; - using Process process = Process.Start(startInfo); - if (process == null) - { - return false; - } - await process.WaitForExitAsync(); - return process.ExitCode == 0; - } - catch - { - return false; - } - } + public async Task OpenAsync(Uri uri, BrowserLaunchOptions options) + { + if (uri == null) + throw new ArgumentNullException(nameof(uri)); + + try + { + var uriString = uri.AbsoluteUri; + + // Use xdg-open which respects user's default browser + var startInfo = new ProcessStartInfo + { + FileName = "xdg-open", + Arguments = $"\"{uriString}\"", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + + using var process = Process.Start(startInfo); + if (process == null) + return false; + + await process.WaitForExitAsync(); + return process.ExitCode == 0; + } + catch + { + return false; + } + } } diff --git a/Services/ClipboardService.cs b/Services/ClipboardService.cs index 6f95621..10fc067 100644 --- a/Services/ClipboardService.cs +++ b/Services/ClipboardService.cs @@ -1,195 +1,206 @@ -using System; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; using System.Diagnostics; -using System.Threading.Tasks; using Microsoft.Maui.ApplicationModel.DataTransfer; namespace Microsoft.Maui.Platform.Linux.Services; +/// +/// Linux clipboard implementation using xclip/xsel command line tools. +/// public class ClipboardService : IClipboard { - private string? _lastSetText; + private string? _lastSetText; - public bool HasText - { - get - { - try - { - return !string.IsNullOrEmpty(GetTextAsync().GetAwaiter().GetResult()); - } - catch - { - return false; - } - } - } + public bool HasText + { + get + { + try + { + var result = GetTextAsync().GetAwaiter().GetResult(); + return !string.IsNullOrEmpty(result); + } + catch + { + return false; + } + } + } - public event EventHandler? ClipboardContentChanged; + public event EventHandler? ClipboardContentChanged; - public async Task GetTextAsync() - { - string text = await TryGetWithXclip(); - if (text != null) - { - return text; - } - return await TryGetWithXsel(); - } + public async Task GetTextAsync() + { + // Try xclip first + var result = await TryGetWithXclip(); + if (result != null) return result; - public async Task SetTextAsync(string? text) - { - _lastSetText = text; - if (string.IsNullOrEmpty(text)) - { - await ClearClipboard(); - return; - } - if (!(await TrySetWithXclip(text))) - { - await TrySetWithXsel(text); - } - this.ClipboardContentChanged?.Invoke(this, EventArgs.Empty); - } + // Try xsel as fallback + return await TryGetWithXsel(); + } - private async Task TryGetWithXclip() - { - _ = 1; - try - { - ProcessStartInfo startInfo = new ProcessStartInfo - { - FileName = "xclip", - Arguments = "-selection clipboard -o", - UseShellExecute = false, - RedirectStandardOutput = true, - RedirectStandardError = true, - CreateNoWindow = true - }; - using Process process = Process.Start(startInfo); - if (process == null) - { - return null; - } - string output = await process.StandardOutput.ReadToEndAsync(); - await process.WaitForExitAsync(); - return (process.ExitCode == 0) ? output : null; - } - catch - { - return null; - } - } + public async Task SetTextAsync(string? text) + { + _lastSetText = text; - private async Task TryGetWithXsel() - { - _ = 1; - try - { - ProcessStartInfo startInfo = new ProcessStartInfo - { - FileName = "xsel", - Arguments = "--clipboard --output", - UseShellExecute = false, - RedirectStandardOutput = true, - RedirectStandardError = true, - CreateNoWindow = true - }; - using Process process = Process.Start(startInfo); - if (process == null) - { - return null; - } - string output = await process.StandardOutput.ReadToEndAsync(); - await process.WaitForExitAsync(); - return (process.ExitCode == 0) ? output : null; - } - catch - { - return null; - } - } + if (string.IsNullOrEmpty(text)) + { + await ClearClipboard(); + return; + } - private async Task TrySetWithXclip(string text) - { - _ = 1; - try - { - ProcessStartInfo startInfo = new ProcessStartInfo - { - FileName = "xclip", - Arguments = "-selection clipboard", - UseShellExecute = false, - RedirectStandardInput = true, - RedirectStandardError = true, - CreateNoWindow = true - }; - using Process process = Process.Start(startInfo); - if (process == null) - { - return false; - } - await process.StandardInput.WriteAsync(text); - process.StandardInput.Close(); - await process.WaitForExitAsync(); - return process.ExitCode == 0; - } - catch - { - return false; - } - } + // Try xclip first + var success = await TrySetWithXclip(text); + if (!success) + { + // Try xsel as fallback + await TrySetWithXsel(text); + } - private async Task TrySetWithXsel(string text) - { - _ = 1; - try - { - ProcessStartInfo startInfo = new ProcessStartInfo - { - FileName = "xsel", - Arguments = "--clipboard --input", - UseShellExecute = false, - RedirectStandardInput = true, - RedirectStandardError = true, - CreateNoWindow = true - }; - using Process process = Process.Start(startInfo); - if (process == null) - { - return false; - } - await process.StandardInput.WriteAsync(text); - process.StandardInput.Close(); - await process.WaitForExitAsync(); - return process.ExitCode == 0; - } - catch - { - return false; - } - } + ClipboardContentChanged?.Invoke(this, EventArgs.Empty); + } - private async Task ClearClipboard() - { - try - { - ProcessStartInfo startInfo = new ProcessStartInfo - { - FileName = "xclip", - Arguments = "-selection clipboard", - UseShellExecute = false, - RedirectStandardInput = true, - CreateNoWindow = true - }; - using Process process = Process.Start(startInfo); - if (process != null) - { - process.StandardInput.Close(); - await process.WaitForExitAsync(); - } - } - catch - { - } - } + private async Task TryGetWithXclip() + { + try + { + var startInfo = new ProcessStartInfo + { + FileName = "xclip", + Arguments = "-selection clipboard -o", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + + using var process = Process.Start(startInfo); + if (process == null) return null; + + var output = await process.StandardOutput.ReadToEndAsync(); + await process.WaitForExitAsync(); + + return process.ExitCode == 0 ? output : null; + } + catch + { + return null; + } + } + + private async Task TryGetWithXsel() + { + try + { + var startInfo = new ProcessStartInfo + { + FileName = "xsel", + Arguments = "--clipboard --output", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + + using var process = Process.Start(startInfo); + if (process == null) return null; + + var output = await process.StandardOutput.ReadToEndAsync(); + await process.WaitForExitAsync(); + + return process.ExitCode == 0 ? output : null; + } + catch + { + return null; + } + } + + private async Task TrySetWithXclip(string text) + { + try + { + var startInfo = new ProcessStartInfo + { + FileName = "xclip", + Arguments = "-selection clipboard", + UseShellExecute = false, + RedirectStandardInput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + + using var process = Process.Start(startInfo); + if (process == null) return false; + + await process.StandardInput.WriteAsync(text); + process.StandardInput.Close(); + + await process.WaitForExitAsync(); + return process.ExitCode == 0; + } + catch + { + return false; + } + } + + private async Task TrySetWithXsel(string text) + { + try + { + var startInfo = new ProcessStartInfo + { + FileName = "xsel", + Arguments = "--clipboard --input", + UseShellExecute = false, + RedirectStandardInput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + + using var process = Process.Start(startInfo); + if (process == null) return false; + + await process.StandardInput.WriteAsync(text); + process.StandardInput.Close(); + + await process.WaitForExitAsync(); + return process.ExitCode == 0; + } + catch + { + return false; + } + } + + private async Task ClearClipboard() + { + try + { + // Try xclip first + var startInfo = new ProcessStartInfo + { + FileName = "xclip", + Arguments = "-selection clipboard", + UseShellExecute = false, + RedirectStandardInput = true, + CreateNoWindow = true + }; + + using var process = Process.Start(startInfo); + if (process != null) + { + process.StandardInput.Close(); + await process.WaitForExitAsync(); + } + } + catch + { + // Ignore errors when clearing + } + } } diff --git a/Services/ColorDialogResult.cs b/Services/ColorDialogResult.cs deleted file mode 100644 index f159461..0000000 --- a/Services/ColorDialogResult.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Microsoft.Maui.Platform.Linux.Services; - -public class ColorDialogResult -{ - public bool Accepted { get; init; } - - public float Red { get; init; } - - public float Green { get; init; } - - public float Blue { get; init; } - - public float Alpha { get; init; } -} diff --git a/Services/ConnectivityService.cs b/Services/ConnectivityService.cs deleted file mode 100644 index 601024c..0000000 --- a/Services/ConnectivityService.cs +++ /dev/null @@ -1,172 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Net.NetworkInformation; -using System.Net.Sockets; -using Microsoft.Maui.Networking; - -namespace Microsoft.Maui.Platform.Linux.Services; - -public class ConnectivityService : IConnectivity, IDisposable -{ - private static readonly Lazy _instance = new Lazy(() => new ConnectivityService()); - - private NetworkAccess _networkAccess; - - private IEnumerable _connectionProfiles; - - private bool _disposed; - - public static ConnectivityService Instance => _instance.Value; - - public NetworkAccess NetworkAccess - { - get - { - //IL_0007: Unknown result type (might be due to invalid IL or missing references) - RefreshConnectivity(); - return _networkAccess; - } - } - - public IEnumerable ConnectionProfiles - { - get - { - RefreshConnectivity(); - return _connectionProfiles; - } - } - - public event EventHandler? ConnectivityChanged; - - public ConnectivityService() - { - _connectionProfiles = new List(); - RefreshConnectivity(); - NetworkChange.NetworkAvailabilityChanged += OnNetworkAvailabilityChanged; - NetworkChange.NetworkAddressChanged += OnNetworkAddressChanged; - } - - private void RefreshConnectivity() - { - //IL_0102: Unknown result type (might be due to invalid IL or missing references) - //IL_0034: Unknown result type (might be due to invalid IL or missing references) - //IL_00f8: Unknown result type (might be due to invalid IL or missing references) - try - { - IEnumerable enumerable = from ni in NetworkInterface.GetAllNetworkInterfaces() - where ni.OperationalStatus == OperationalStatus.Up && ni.NetworkInterfaceType != NetworkInterfaceType.Loopback - select ni; - if (!enumerable.Any()) - { - _networkAccess = (NetworkAccess)1; - _connectionProfiles = Enumerable.Empty(); - return; - } - List list = new List(); - using (IEnumerator enumerator = enumerable.GetEnumerator()) - { - while (enumerator.MoveNext()) - { - switch (enumerator.Current.NetworkInterfaceType) - { - case NetworkInterfaceType.Ethernet: - case NetworkInterfaceType.FastEthernetT: - case NetworkInterfaceType.FastEthernetFx: - case NetworkInterfaceType.GigabitEthernet: - list.Add((ConnectionProfile)3); - break; - case NetworkInterfaceType.Wireless80211: - list.Add((ConnectionProfile)4); - break; - case NetworkInterfaceType.Ppp: - case NetworkInterfaceType.Slip: - list.Add((ConnectionProfile)2); - break; - default: - list.Add((ConnectionProfile)0); - break; - } - } - } - _connectionProfiles = list.Distinct().ToList(); - _networkAccess = (NetworkAccess)(CheckInternetAccess() ? 4 : ((!_connectionProfiles.Any()) ? 1 : 2)); - } - catch - { - _networkAccess = (NetworkAccess)0; - _connectionProfiles = (IEnumerable)(object)new ConnectionProfile[1]; - } - } - - private bool CheckInternetAccess() - { - try - { - return Dns.GetHostEntry("dns.google").AddressList.Length != 0; - } - catch - { - try - { - foreach (NetworkInterface item in from n in NetworkInterface.GetAllNetworkInterfaces() - where n.OperationalStatus == OperationalStatus.Up - select n) - { - if (item.GetIPProperties().GatewayAddresses.Any((GatewayIPAddressInformation g) => g.Address.AddressFamily == AddressFamily.InterNetwork)) - { - return true; - } - } - } - catch - { - } - return false; - } - } - - private void OnNetworkAvailabilityChanged(object? sender, NetworkAvailabilityEventArgs e) - { - //IL_0001: Unknown result type (might be due to invalid IL or missing references) - //IL_0019: Unknown result type (might be due to invalid IL or missing references) - //IL_003b: Unknown result type (might be due to invalid IL or missing references) - //IL_0046: Unknown result type (might be due to invalid IL or missing references) - //IL_0050: Expected O, but got Unknown - NetworkAccess networkAccess = _networkAccess; - List first = _connectionProfiles.ToList(); - RefreshConnectivity(); - if (networkAccess != _networkAccess || !first.SequenceEqual(_connectionProfiles)) - { - this.ConnectivityChanged?.Invoke(this, new ConnectivityChangedEventArgs(_networkAccess, _connectionProfiles)); - } - } - - private void OnNetworkAddressChanged(object? sender, EventArgs e) - { - //IL_0001: Unknown result type (might be due to invalid IL or missing references) - //IL_0019: Unknown result type (might be due to invalid IL or missing references) - //IL_003b: Unknown result type (might be due to invalid IL or missing references) - //IL_0046: Unknown result type (might be due to invalid IL or missing references) - //IL_0050: Expected O, but got Unknown - NetworkAccess networkAccess = _networkAccess; - List first = _connectionProfiles.ToList(); - RefreshConnectivity(); - if (networkAccess != _networkAccess || !first.SequenceEqual(_connectionProfiles)) - { - this.ConnectivityChanged?.Invoke(this, new ConnectivityChangedEventArgs(_networkAccess, _connectionProfiles)); - } - } - - public void Dispose() - { - if (!_disposed) - { - _disposed = true; - NetworkChange.NetworkAvailabilityChanged -= OnNetworkAvailabilityChanged; - NetworkChange.NetworkAddressChanged -= OnNetworkAddressChanged; - } - } -} diff --git a/Services/DesktopEnvironment.cs b/Services/DesktopEnvironment.cs deleted file mode 100644 index e8a7952..0000000 --- a/Services/DesktopEnvironment.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Microsoft.Maui.Platform.Linux.Services; - -public enum DesktopEnvironment -{ - Unknown, - GNOME, - KDE, - XFCE, - MATE, - Cinnamon, - LXQt, - LXDE -} diff --git a/Services/DeviceDisplayService.cs b/Services/DeviceDisplayService.cs deleted file mode 100644 index 7584172..0000000 --- a/Services/DeviceDisplayService.cs +++ /dev/null @@ -1,131 +0,0 @@ -using System; -using System.Diagnostics; -using Microsoft.Maui.Devices; -using Microsoft.Maui.Platform.Linux.Native; - -namespace Microsoft.Maui.Platform.Linux.Services; - -public class DeviceDisplayService : IDeviceDisplay -{ - private static readonly Lazy _instance = new Lazy(() => new DeviceDisplayService()); - - private DisplayInfo _mainDisplayInfo; - - private bool _keepScreenOn; - - public static DeviceDisplayService Instance => _instance.Value; - - public bool KeepScreenOn - { - get - { - return _keepScreenOn; - } - set - { - if (_keepScreenOn != value) - { - _keepScreenOn = value; - SetScreenSaverInhibit(value); - } - } - } - - public DisplayInfo MainDisplayInfo - { - get - { - //IL_0007: Unknown result type (might be due to invalid IL or missing references) - RefreshDisplayInfo(); - return _mainDisplayInfo; - } - } - - public event EventHandler? MainDisplayInfoChanged; - - public DeviceDisplayService() - { - RefreshDisplayInfo(); - } - - private void RefreshDisplayInfo() - { - //IL_0097: Unknown result type (might be due to invalid IL or missing references) - //IL_009c: Unknown result type (might be due to invalid IL or missing references) - //IL_0067: Unknown result type (might be due to invalid IL or missing references) - //IL_006c: Unknown result type (might be due to invalid IL or missing references) - //IL_0038: Unknown result type (might be due to invalid IL or missing references) - //IL_003d: Unknown result type (might be due to invalid IL or missing references) - try - { - IntPtr intPtr = GdkNative.gdk_screen_get_default(); - if (intPtr != IntPtr.Zero) - { - int num = GdkNative.gdk_screen_get_width(intPtr); - int num2 = GdkNative.gdk_screen_get_height(intPtr); - double scaleFactor = GetScaleFactor(); - _mainDisplayInfo = new DisplayInfo((double)num, (double)num2, scaleFactor, (DisplayOrientation)((num <= num2) ? 1 : 2), (DisplayRotation)1, GetRefreshRate()); - } - else - { - _mainDisplayInfo = new DisplayInfo(1920.0, 1080.0, 1.0, (DisplayOrientation)2, (DisplayRotation)1, 60f); - } - } - catch - { - _mainDisplayInfo = new DisplayInfo(1920.0, 1080.0, 1.0, (DisplayOrientation)2, (DisplayRotation)1, 60f); - } - } - - private double GetScaleFactor() - { - string environmentVariable = Environment.GetEnvironmentVariable("GDK_SCALE"); - if (!string.IsNullOrEmpty(environmentVariable) && double.TryParse(environmentVariable, out var result)) - { - return result; - } - string environmentVariable2 = Environment.GetEnvironmentVariable("QT_SCALE_FACTOR"); - if (!string.IsNullOrEmpty(environmentVariable2) && double.TryParse(environmentVariable2, out result)) - { - return result; - } - return 1.0; - } - - private float GetRefreshRate() - { - return 60f; - } - - private void SetScreenSaverInhibit(bool inhibit) - { - try - { - string value = (inhibit ? "suspend" : "resume"); - IntPtr intPtr = LinuxApplication.Current?.MainWindow?.Handle ?? IntPtr.Zero; - if (intPtr != IntPtr.Zero) - { - long value2 = intPtr.ToInt64(); - Process.Start(new ProcessStartInfo - { - FileName = "xdg-screensaver", - Arguments = $"{value} {value2}", - UseShellExecute = false, - CreateNoWindow = true - }); - } - } - catch - { - } - } - - public void OnDisplayInfoChanged() - { - //IL_0013: Unknown result type (might be due to invalid IL or missing references) - //IL_0018: Unknown result type (might be due to invalid IL or missing references) - //IL_0022: Expected O, but got Unknown - RefreshDisplayInfo(); - this.MainDisplayInfoChanged?.Invoke(this, new DisplayInfoChangedEventArgs(_mainDisplayInfo)); - } -} diff --git a/Services/DeviceInfoService.cs b/Services/DeviceInfoService.cs deleted file mode 100644 index 35d4c15..0000000 --- a/Services/DeviceInfoService.cs +++ /dev/null @@ -1,93 +0,0 @@ -using System; -using System.IO; -using Microsoft.Maui.Devices; - -namespace Microsoft.Maui.Platform.Linux.Services; - -public class DeviceInfoService : IDeviceInfo -{ - private static readonly Lazy _instance = new Lazy(() => new DeviceInfoService()); - - private string? _model; - - private string? _manufacturer; - - private string? _name; - - private string? _versionString; - - public static DeviceInfoService Instance => _instance.Value; - - public string Model => _model ?? "Linux Desktop"; - - public string Manufacturer => _manufacturer ?? "Unknown"; - - public string Name => _name ?? Environment.MachineName; - - public string VersionString => _versionString ?? Environment.OSVersion.VersionString; - - public Version Version - { - get - { - try - { - if (System.Version.TryParse(Environment.OSVersion.Version.ToString(), out Version result)) - { - return result; - } - } - catch - { - } - return new Version(1, 0); - } - } - - public DevicePlatform Platform => DevicePlatform.Create("Linux"); - - public DeviceIdiom Idiom => DeviceIdiom.Desktop; - - public DeviceType DeviceType => (DeviceType)1; - - public DeviceInfoService() - { - LoadDeviceInfo(); - } - - private void LoadDeviceInfo() - { - try - { - if (File.Exists("/sys/class/dmi/id/product_name")) - { - _model = File.ReadAllText("/sys/class/dmi/id/product_name").Trim(); - } - if (File.Exists("/sys/class/dmi/id/sys_vendor")) - { - _manufacturer = File.ReadAllText("/sys/class/dmi/id/sys_vendor").Trim(); - } - _name = Environment.MachineName; - _versionString = Environment.OSVersion.VersionString; - } - catch - { - if (_model == null) - { - _model = "Linux Desktop"; - } - if (_manufacturer == null) - { - _manufacturer = "Unknown"; - } - if (_name == null) - { - _name = "localhost"; - } - if (_versionString == null) - { - _versionString = "Linux"; - } - } - } -} diff --git a/Services/DisplayServerFactory.cs b/Services/DisplayServerFactory.cs index 90f1bdc..58a8e19 100644 --- a/Services/DisplayServerFactory.cs +++ b/Services/DisplayServerFactory.cs @@ -1,100 +1,274 @@ -using System; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; +using Microsoft.Maui.Platform.Linux.Window; +using Microsoft.Maui.Platform.Linux.Rendering; namespace Microsoft.Maui.Platform.Linux.Services; +/// +/// Supported display server types. +/// +public enum DisplayServerType +{ + Auto, + X11, + Wayland +} + +/// +/// Factory for creating display server connections. +/// Supports X11 and Wayland display servers. +/// public static class DisplayServerFactory { - private static DisplayServerType? _cachedServerType; + private static DisplayServerType? _cachedServerType; - public static DisplayServerType DetectDisplayServer() - { - if (_cachedServerType.HasValue) - { - return _cachedServerType.Value; - } - if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("WAYLAND_DISPLAY"))) - { - string? environmentVariable = Environment.GetEnvironmentVariable("DISPLAY"); - string environmentVariable2 = Environment.GetEnvironmentVariable("MAUI_PREFER_X11"); - if (!string.IsNullOrEmpty(environmentVariable) && !string.IsNullOrEmpty(environmentVariable2)) - { - Console.WriteLine("[DisplayServer] XWayland detected, using X11 backend (MAUI_PREFER_X11 set)"); - _cachedServerType = DisplayServerType.X11; - return DisplayServerType.X11; - } - Console.WriteLine("[DisplayServer] Wayland display detected"); - _cachedServerType = DisplayServerType.Wayland; - return DisplayServerType.Wayland; - } - if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("DISPLAY"))) - { - Console.WriteLine("[DisplayServer] X11 display detected"); - _cachedServerType = DisplayServerType.X11; - return DisplayServerType.X11; - } - Console.WriteLine("[DisplayServer] No display server detected, defaulting to X11"); - _cachedServerType = DisplayServerType.X11; - return DisplayServerType.X11; - } + /// + /// Detects the current display server type. + /// + public static DisplayServerType DetectDisplayServer() + { + if (_cachedServerType.HasValue) + return _cachedServerType.Value; - public static IDisplayWindow CreateWindow(string title, int width, int height, DisplayServerType serverType = DisplayServerType.Auto) - { - if (serverType == DisplayServerType.Auto) - { - serverType = DetectDisplayServer(); - } - return serverType switch - { - DisplayServerType.X11 => CreateX11Window(title, width, height), - DisplayServerType.Wayland => CreateWaylandWindow(title, width, height), - _ => CreateX11Window(title, width, height), - }; - } + // Check for Wayland first (modern default) + var waylandDisplay = Environment.GetEnvironmentVariable("WAYLAND_DISPLAY"); + if (!string.IsNullOrEmpty(waylandDisplay)) + { + // Check if XWayland is available - prefer it for now until native Wayland is fully tested + var xDisplay = Environment.GetEnvironmentVariable("DISPLAY"); + var preferX11 = Environment.GetEnvironmentVariable("MAUI_PREFER_X11"); + + if (!string.IsNullOrEmpty(xDisplay) && !string.IsNullOrEmpty(preferX11)) + { + Console.WriteLine("[DisplayServer] XWayland detected, using X11 backend (MAUI_PREFER_X11 set)"); + _cachedServerType = DisplayServerType.X11; + return DisplayServerType.X11; + } - private static IDisplayWindow CreateX11Window(string title, int width, int height) - { - try - { - Console.WriteLine($"[DisplayServer] Creating X11 window: {title} ({width}x{height})"); - return new X11DisplayWindow(title, width, height); - } - catch (Exception ex) - { - Console.WriteLine("[DisplayServer] Failed to create X11 window: " + ex.Message); - throw; - } - } + Console.WriteLine("[DisplayServer] Wayland display detected"); + _cachedServerType = DisplayServerType.Wayland; + return DisplayServerType.Wayland; + } - private static IDisplayWindow CreateWaylandWindow(string title, int width, int height) - { - try - { - Console.WriteLine($"[DisplayServer] Creating Wayland window: {title} ({width}x{height})"); - return new WaylandDisplayWindow(title, width, height); - } - catch (Exception ex) - { - Console.WriteLine("[DisplayServer] Failed to create Wayland window: " + ex.Message); - if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("DISPLAY"))) - { - Console.WriteLine("[DisplayServer] Falling back to X11 (XWayland)"); - return CreateX11Window(title, width, height); - } - throw; - } - } + // Fall back to X11 + var x11Display = Environment.GetEnvironmentVariable("DISPLAY"); + if (!string.IsNullOrEmpty(x11Display)) + { + Console.WriteLine("[DisplayServer] X11 display detected"); + _cachedServerType = DisplayServerType.X11; + return DisplayServerType.X11; + } - public static string GetDisplayServerName(DisplayServerType serverType = DisplayServerType.Auto) - { - if (serverType == DisplayServerType.Auto) - { - serverType = DetectDisplayServer(); - } - return serverType switch - { - DisplayServerType.X11 => "X11", - DisplayServerType.Wayland => "Wayland", - _ => "Unknown", - }; - } + // Default to X11 and let it fail if not available + Console.WriteLine("[DisplayServer] No display server detected, defaulting to X11"); + _cachedServerType = DisplayServerType.X11; + return DisplayServerType.X11; + } + + /// + /// Creates a window for the specified or detected display server. + /// + public static IDisplayWindow CreateWindow(string title, int width, int height, DisplayServerType serverType = DisplayServerType.Auto) + { + if (serverType == DisplayServerType.Auto) + { + serverType = DetectDisplayServer(); + } + + return serverType switch + { + DisplayServerType.X11 => CreateX11Window(title, width, height), + DisplayServerType.Wayland => CreateWaylandWindow(title, width, height), + _ => CreateX11Window(title, width, height) + }; + } + + private static IDisplayWindow CreateX11Window(string title, int width, int height) + { + try + { + Console.WriteLine($"[DisplayServer] Creating X11 window: {title} ({width}x{height})"); + return new X11DisplayWindow(title, width, height); + } + catch (Exception ex) + { + Console.WriteLine($"[DisplayServer] Failed to create X11 window: {ex.Message}"); + throw; + } + } + + private static IDisplayWindow CreateWaylandWindow(string title, int width, int height) + { + try + { + Console.WriteLine($"[DisplayServer] Creating Wayland window: {title} ({width}x{height})"); + return new WaylandDisplayWindow(title, width, height); + } + catch (Exception ex) + { + Console.WriteLine($"[DisplayServer] Failed to create Wayland window: {ex.Message}"); + + // Try to fall back to X11 via XWayland + var xDisplay = Environment.GetEnvironmentVariable("DISPLAY"); + if (!string.IsNullOrEmpty(xDisplay)) + { + Console.WriteLine("[DisplayServer] Falling back to X11 (XWayland)"); + return CreateX11Window(title, width, height); + } + + throw; + } + } + + /// + /// Gets a human-readable name for the display server. + /// + public static string GetDisplayServerName(DisplayServerType serverType = DisplayServerType.Auto) + { + if (serverType == DisplayServerType.Auto) + serverType = DetectDisplayServer(); + + return serverType switch + { + DisplayServerType.X11 => "X11", + DisplayServerType.Wayland => "Wayland", + _ => "Unknown" + }; + } +} + +/// +/// Common interface for display server windows. +/// +public interface IDisplayWindow : IDisposable +{ + int Width { get; } + int Height { get; } + bool IsRunning { get; } + void Show(); + void Hide(); + void SetTitle(string title); + void Resize(int width, int height); + void ProcessEvents(); + void Stop(); + event EventHandler? KeyDown; + event EventHandler? KeyUp; + event EventHandler? TextInput; + event EventHandler? PointerMoved; + event EventHandler? PointerPressed; + event EventHandler? PointerReleased; + event EventHandler? Scroll; + event EventHandler? Exposed; + event EventHandler<(int Width, int Height)>? Resized; + event EventHandler? CloseRequested; +} + +/// +/// X11 display window wrapper implementing the common interface. +/// +public class X11DisplayWindow : IDisplayWindow +{ + private readonly X11Window _window; + + public int Width => _window.Width; + public int Height => _window.Height; + public bool IsRunning => _window.IsRunning; + + public event EventHandler? KeyDown; + public event EventHandler? KeyUp; + public event EventHandler? TextInput; + public event EventHandler? PointerMoved; + public event EventHandler? PointerPressed; + public event EventHandler? PointerReleased; + public event EventHandler? Scroll; + public event EventHandler? Exposed; + public event EventHandler<(int Width, int Height)>? Resized; + public event EventHandler? CloseRequested; + + public X11DisplayWindow(string title, int width, int height) + { + _window = new X11Window(title, width, height); + + _window.KeyDown += (s, e) => KeyDown?.Invoke(this, e); + _window.KeyUp += (s, e) => KeyUp?.Invoke(this, e); + _window.TextInput += (s, e) => TextInput?.Invoke(this, e); + _window.PointerMoved += (s, e) => PointerMoved?.Invoke(this, e); + _window.PointerPressed += (s, e) => PointerPressed?.Invoke(this, e); + _window.PointerReleased += (s, e) => PointerReleased?.Invoke(this, e); + _window.Scroll += (s, e) => Scroll?.Invoke(this, e); + _window.Exposed += (s, e) => Exposed?.Invoke(this, e); + _window.Resized += (s, e) => Resized?.Invoke(this, e); + _window.CloseRequested += (s, e) => CloseRequested?.Invoke(this, e); + } + + public void Show() => _window.Show(); + public void Hide() => _window.Hide(); + public void SetTitle(string title) => _window.SetTitle(title); + public void Resize(int width, int height) => _window.Resize(width, height); + public void ProcessEvents() => _window.ProcessEvents(); + public void Stop() => _window.Stop(); + public void Dispose() => _window.Dispose(); +} + +/// +/// Wayland display window wrapper implementing IDisplayWindow. +/// Uses the full WaylandWindow implementation with xdg-shell protocol. +/// +public class WaylandDisplayWindow : IDisplayWindow +{ + private readonly WaylandWindow _window; + + public int Width => _window.Width; + public int Height => _window.Height; + public bool IsRunning => _window.IsRunning; + + /// + /// Gets the pixel data pointer for rendering. + /// + public IntPtr PixelData => _window.PixelData; + + /// + /// Gets the stride (bytes per row) of the pixel buffer. + /// + public int Stride => _window.Stride; + + public event EventHandler? KeyDown; + public event EventHandler? KeyUp; + public event EventHandler? TextInput; + public event EventHandler? PointerMoved; + public event EventHandler? PointerPressed; + public event EventHandler? PointerReleased; + public event EventHandler? Scroll; + public event EventHandler? Exposed; + public event EventHandler<(int Width, int Height)>? Resized; + public event EventHandler? CloseRequested; + + public WaylandDisplayWindow(string title, int width, int height) + { + _window = new WaylandWindow(title, width, height); + + // Wire up events + _window.KeyDown += (s, e) => KeyDown?.Invoke(this, e); + _window.KeyUp += (s, e) => KeyUp?.Invoke(this, e); + _window.TextInput += (s, e) => TextInput?.Invoke(this, e); + _window.PointerMoved += (s, e) => PointerMoved?.Invoke(this, e); + _window.PointerPressed += (s, e) => PointerPressed?.Invoke(this, e); + _window.PointerReleased += (s, e) => PointerReleased?.Invoke(this, e); + _window.Scroll += (s, e) => Scroll?.Invoke(this, e); + _window.Exposed += (s, e) => Exposed?.Invoke(this, e); + _window.Resized += (s, e) => Resized?.Invoke(this, e); + _window.CloseRequested += (s, e) => CloseRequested?.Invoke(this, e); + } + + public void Show() => _window.Show(); + public void Hide() => _window.Hide(); + public void SetTitle(string title) => _window.SetTitle(title); + public void Resize(int width, int height) => _window.Resize(width, height); + public void ProcessEvents() => _window.ProcessEvents(); + public void Stop() => _window.Stop(); + public void CommitFrame() => _window.CommitFrame(); + public void Dispose() => _window.Dispose(); } diff --git a/Services/DisplayServerType.cs b/Services/DisplayServerType.cs deleted file mode 100644 index 6700a57..0000000 --- a/Services/DisplayServerType.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Microsoft.Maui.Platform.Linux.Services; - -public enum DisplayServerType -{ - Auto, - X11, - Wayland -} diff --git a/Services/DragAction.cs b/Services/DragAction.cs deleted file mode 100644 index d68e42f..0000000 --- a/Services/DragAction.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Microsoft.Maui.Platform.Linux.Services; - -public enum DragAction -{ - None, - Copy, - Move, - Link -} diff --git a/Services/DragData.cs b/Services/DragData.cs deleted file mode 100644 index a5290df..0000000 --- a/Services/DragData.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; - -namespace Microsoft.Maui.Platform.Linux.Services; - -public class DragData -{ - public IntPtr SourceWindow { get; set; } - - public IntPtr[] SupportedTypes { get; set; } = Array.Empty(); - - public string? Text { get; set; } - - public string[]? FilePaths { get; set; } - - public object? Data { get; set; } -} diff --git a/Services/DragDropService.cs b/Services/DragDropService.cs index bc9f11b..53b147a 100644 --- a/Services/DragDropService.cs +++ b/Services/DragDropService.cs @@ -1,361 +1,516 @@ -using System; -using System.Collections.Generic; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using System.Runtime.InteropServices; +using System.Text; namespace Microsoft.Maui.Platform.Linux.Services; +/// +/// Provides drag and drop functionality using the X11 XDND protocol. +/// public class DragDropService : IDisposable { - private struct XClientMessageEvent - { - public int type; - - public ulong serial; - - public bool send_event; - - public IntPtr display; - - public IntPtr window; - - public IntPtr message_type; - - public int format; - - public IntPtr data0; - - public IntPtr data1; - - public IntPtr data2; - - public IntPtr data3; - - public IntPtr data4; - } - - private IntPtr _display; - - private IntPtr _window; - - private bool _isDragging; - - private DragData? _currentDragData; - - private IntPtr _dragSource; - - private IntPtr _dragTarget; - - private bool _disposed; - - private IntPtr _xdndAware; - - private IntPtr _xdndEnter; - - private IntPtr _xdndPosition; - - private IntPtr _xdndStatus; - - private IntPtr _xdndLeave; - - private IntPtr _xdndDrop; - - private IntPtr _xdndFinished; - - private IntPtr _xdndSelection; - - private IntPtr _xdndActionCopy; - - private IntPtr _xdndActionMove; - - private IntPtr _xdndActionLink; - - private IntPtr _xdndTypeList; - - private IntPtr _textPlain; - - private IntPtr _textUri; - - private IntPtr _applicationOctetStream; - - private const int ClientMessage = 33; - - private const int PropModeReplace = 0; - - private static readonly IntPtr XA_ATOM = (IntPtr)4; - - public bool IsDragging => _isDragging; - - public event EventHandler? DragEnter; - - public event EventHandler? DragOver; - - public event EventHandler? DragLeave; - - public event EventHandler? Drop; - - public void Initialize(IntPtr display, IntPtr window) - { - _display = display; - _window = window; - InitializeAtoms(); - SetXdndAware(); - } - - private void InitializeAtoms() - { - _xdndAware = XInternAtom(_display, "XdndAware", onlyIfExists: false); - _xdndEnter = XInternAtom(_display, "XdndEnter", onlyIfExists: false); - _xdndPosition = XInternAtom(_display, "XdndPosition", onlyIfExists: false); - _xdndStatus = XInternAtom(_display, "XdndStatus", onlyIfExists: false); - _xdndLeave = XInternAtom(_display, "XdndLeave", onlyIfExists: false); - _xdndDrop = XInternAtom(_display, "XdndDrop", onlyIfExists: false); - _xdndFinished = XInternAtom(_display, "XdndFinished", onlyIfExists: false); - _xdndSelection = XInternAtom(_display, "XdndSelection", onlyIfExists: false); - _xdndActionCopy = XInternAtom(_display, "XdndActionCopy", onlyIfExists: false); - _xdndActionMove = XInternAtom(_display, "XdndActionMove", onlyIfExists: false); - _xdndActionLink = XInternAtom(_display, "XdndActionLink", onlyIfExists: false); - _xdndTypeList = XInternAtom(_display, "XdndTypeList", onlyIfExists: false); - _textPlain = XInternAtom(_display, "text/plain", onlyIfExists: false); - _textUri = XInternAtom(_display, "text/uri-list", onlyIfExists: false); - _applicationOctetStream = XInternAtom(_display, "application/octet-stream", onlyIfExists: false); - } - - private void SetXdndAware() - { - int data = 5; - XChangeProperty(_display, _window, _xdndAware, XA_ATOM, 32, 0, ref data, 1); - } - - public bool ProcessClientMessage(IntPtr messageType, IntPtr[] data) - { - if (messageType == _xdndEnter) - { - return HandleXdndEnter(data); - } - if (messageType == _xdndPosition) - { - return HandleXdndPosition(data); - } - if (messageType == _xdndLeave) - { - return HandleXdndLeave(data); - } - if (messageType == _xdndDrop) - { - return HandleXdndDrop(data); - } - return false; - } - - private bool HandleXdndEnter(IntPtr[] data) - { - _dragSource = data[0]; - _ = data[1]; - bool num = ((nint)data[1] & 1) != 0; - List list = new List(); - if (num) - { - list = GetTypeList(_dragSource); - } - else - { - for (int i = 2; i < 5; i++) - { - if (data[i] != IntPtr.Zero) - { - list.Add(data[i]); - } - } - } - _currentDragData = new DragData - { - SourceWindow = _dragSource, - SupportedTypes = list.ToArray() - }; - this.DragEnter?.Invoke(this, new DragEventArgs(_currentDragData, 0, 0)); - return true; - } - - private bool HandleXdndPosition(IntPtr[] data) - { - if (_currentDragData == null) - { - return false; - } - int x = (int)(((nint)data[2] >> 16) & 0xFFFF); - int y = (int)((nint)data[2] & 0xFFFF); - IntPtr atom = data[4]; - DragEventArgs e = new DragEventArgs(_currentDragData, x, y) - { - AllowedAction = GetDragAction(atom) - }; - this.DragOver?.Invoke(this, e); - SendXdndStatus(e.Accepted, e.AcceptedAction); - return true; - } - - private bool HandleXdndLeave(IntPtr[] data) - { - _currentDragData = null; - _dragSource = IntPtr.Zero; - this.DragLeave?.Invoke(this, EventArgs.Empty); - return true; - } - - private bool HandleXdndDrop(IntPtr[] data) - { - if (_currentDragData == null) - { - return false; - } - uint timestamp = (uint)(nint)data[2]; - string droppedData = RequestDropData(timestamp); - DropEventArgs e = new DropEventArgs(_currentDragData, droppedData); - this.Drop?.Invoke(this, e); - SendXdndFinished(e.Handled); - _currentDragData = null; - _dragSource = IntPtr.Zero; - return true; - } - - private List GetTypeList(IntPtr window) - { - List list = new List(); - if (XGetWindowProperty(_display, window, _xdndTypeList, 0L, 1024L, delete: false, XA_ATOM, out var _, out var _, out var nitems, out var _, out var data) == 0 && data != IntPtr.Zero) - { - for (int i = 0; i < (int)(nint)nitems; i++) - { - IntPtr item = Marshal.ReadIntPtr(data, i * IntPtr.Size); - list.Add(item); - } - XFree(data); - } - return list; - } - - private void SendXdndStatus(bool accepted, DragAction action) - { - XClientMessageEvent xevent = new XClientMessageEvent - { - type = 33, - window = _dragSource, - message_type = _xdndStatus, - format = 32 - }; - xevent.data0 = _window; - xevent.data1 = (IntPtr)(accepted ? 1 : 0); - xevent.data2 = (IntPtr)0; - xevent.data3 = (IntPtr)0; - xevent.data4 = GetActionAtom(action); - XSendEvent(_display, _dragSource, propagate: false, 0L, ref xevent); - XFlush(_display); - } - - private void SendXdndFinished(bool accepted) - { - XClientMessageEvent xevent = new XClientMessageEvent - { - type = 33, - window = _dragSource, - message_type = _xdndFinished, - format = 32 - }; - xevent.data0 = _window; - xevent.data1 = (IntPtr)(accepted ? 1 : 0); - xevent.data2 = (accepted ? _xdndActionCopy : IntPtr.Zero); - XSendEvent(_display, _dragSource, propagate: false, 0L, ref xevent); - XFlush(_display); - } - - private string? RequestDropData(uint timestamp) - { - IntPtr target = _textPlain; - if (_currentDragData != null) - { - IntPtr[] supportedTypes = _currentDragData.SupportedTypes; - for (int i = 0; i < supportedTypes.Length; i++) - { - if (supportedTypes[i] == _textUri) - { - target = _textUri; - break; - } - } - } - XConvertSelection(_display, _xdndSelection, target, _xdndSelection, _window, timestamp); - XFlush(_display); - return null; - } - - private DragAction GetDragAction(IntPtr atom) - { - if (atom == _xdndActionCopy) - { - return DragAction.Copy; - } - if (atom == _xdndActionMove) - { - return DragAction.Move; - } - if (atom == _xdndActionLink) - { - return DragAction.Link; - } - return DragAction.None; - } - - private IntPtr GetActionAtom(DragAction action) - { - return action switch - { - DragAction.Copy => _xdndActionCopy, - DragAction.Move => _xdndActionMove, - DragAction.Link => _xdndActionLink, - _ => IntPtr.Zero, - }; - } - - public void StartDrag(DragData data) - { - if (!_isDragging) - { - _isDragging = true; - _currentDragData = data; - } - } - - public void CancelDrag() - { - _isDragging = false; - _currentDragData = null; - } - - public void Dispose() - { - if (!_disposed) - { - _disposed = true; - } - } - - [DllImport("libX11.so.6")] - private static extern IntPtr XInternAtom(IntPtr display, string atomName, bool onlyIfExists); - - [DllImport("libX11.so.6")] - private static extern int XChangeProperty(IntPtr display, IntPtr window, IntPtr property, IntPtr type, int format, int mode, ref int data, int nelements); - - [DllImport("libX11.so.6")] - private static extern int XGetWindowProperty(IntPtr display, IntPtr window, IntPtr property, long offset, long length, bool delete, IntPtr reqType, out IntPtr actualType, out int actualFormat, out IntPtr nitems, out IntPtr bytesAfter, out IntPtr data); - - [DllImport("libX11.so.6")] - private static extern int XSendEvent(IntPtr display, IntPtr window, bool propagate, long eventMask, ref XClientMessageEvent xevent); - - [DllImport("libX11.so.6")] - private static extern int XConvertSelection(IntPtr display, IntPtr selection, IntPtr target, IntPtr property, IntPtr requestor, uint time); - - [DllImport("libX11.so.6")] - private static extern void XFree(IntPtr ptr); - - [DllImport("libX11.so.6")] - private static extern void XFlush(IntPtr display); + private nint _display; + private nint _window; + private bool _isDragging; + private DragData? _currentDragData; + private nint _dragSource; + private nint _dragTarget; + private bool _disposed; + + // XDND atoms + private nint _xdndAware; + private nint _xdndEnter; + private nint _xdndPosition; + private nint _xdndStatus; + private nint _xdndLeave; + private nint _xdndDrop; + private nint _xdndFinished; + private nint _xdndSelection; + private nint _xdndActionCopy; + private nint _xdndActionMove; + private nint _xdndActionLink; + private nint _xdndTypeList; + + // Common MIME types + private nint _textPlain; + private nint _textUri; + private nint _applicationOctetStream; + + /// + /// Gets whether a drag operation is in progress. + /// + public bool IsDragging => _isDragging; + + /// + /// Event raised when a drag enters the window. + /// + public event EventHandler? DragEnter; + + /// + /// Event raised when dragging over the window. + /// + public event EventHandler? DragOver; + + /// + /// Event raised when a drag leaves the window. + /// + public event EventHandler? DragLeave; + + /// + /// Event raised when a drop occurs. + /// + public event EventHandler? Drop; + + /// + /// Initializes the drag drop service for the specified window. + /// + public void Initialize(nint display, nint window) + { + _display = display; + _window = window; + + InitializeAtoms(); + SetXdndAware(); + } + + private void InitializeAtoms() + { + _xdndAware = XInternAtom(_display, "XdndAware", false); + _xdndEnter = XInternAtom(_display, "XdndEnter", false); + _xdndPosition = XInternAtom(_display, "XdndPosition", false); + _xdndStatus = XInternAtom(_display, "XdndStatus", false); + _xdndLeave = XInternAtom(_display, "XdndLeave", false); + _xdndDrop = XInternAtom(_display, "XdndDrop", false); + _xdndFinished = XInternAtom(_display, "XdndFinished", false); + _xdndSelection = XInternAtom(_display, "XdndSelection", false); + _xdndActionCopy = XInternAtom(_display, "XdndActionCopy", false); + _xdndActionMove = XInternAtom(_display, "XdndActionMove", false); + _xdndActionLink = XInternAtom(_display, "XdndActionLink", false); + _xdndTypeList = XInternAtom(_display, "XdndTypeList", false); + + _textPlain = XInternAtom(_display, "text/plain", false); + _textUri = XInternAtom(_display, "text/uri-list", false); + _applicationOctetStream = XInternAtom(_display, "application/octet-stream", false); + } + + private void SetXdndAware() + { + // Set XdndAware property to indicate we support XDND version 5 + int version = 5; + XChangeProperty(_display, _window, _xdndAware, XA_ATOM, 32, + PropModeReplace, ref version, 1); + } + + /// + /// Processes an X11 client message for drag and drop. + /// + public bool ProcessClientMessage(nint messageType, nint[] data) + { + if (messageType == _xdndEnter) + { + return HandleXdndEnter(data); + } + else if (messageType == _xdndPosition) + { + return HandleXdndPosition(data); + } + else if (messageType == _xdndLeave) + { + return HandleXdndLeave(data); + } + else if (messageType == _xdndDrop) + { + return HandleXdndDrop(data); + } + + return false; + } + + private bool HandleXdndEnter(nint[] data) + { + _dragSource = data[0]; + int version = (int)((data[1] >> 24) & 0xFF); + bool hasTypeList = (data[1] & 1) != 0; + + var types = new List(); + + if (hasTypeList) + { + // Get types from XdndTypeList property + types = GetTypeList(_dragSource); + } + else + { + // Types are in the message + for (int i = 2; i < 5; i++) + { + if (data[i] != IntPtr.Zero) + { + types.Add(data[i]); + } + } + } + + _currentDragData = new DragData + { + SourceWindow = _dragSource, + SupportedTypes = types.ToArray() + }; + + DragEnter?.Invoke(this, new DragEventArgs(_currentDragData, 0, 0)); + return true; + } + + private bool HandleXdndPosition(nint[] data) + { + if (_currentDragData == null) return false; + + int x = (int)((data[2] >> 16) & 0xFFFF); + int y = (int)(data[2] & 0xFFFF); + nint action = data[4]; + + var eventArgs = new DragEventArgs(_currentDragData, x, y) + { + AllowedAction = GetDragAction(action) + }; + + DragOver?.Invoke(this, eventArgs); + + // Send XdndStatus reply + SendXdndStatus(eventArgs.Accepted, eventArgs.AcceptedAction); + + return true; + } + + private bool HandleXdndLeave(nint[] data) + { + _currentDragData = null; + _dragSource = IntPtr.Zero; + DragLeave?.Invoke(this, EventArgs.Empty); + return true; + } + + private bool HandleXdndDrop(nint[] data) + { + if (_currentDragData == null) return false; + + uint timestamp = (uint)data[2]; + + // Request the data + string? droppedData = RequestDropData(timestamp); + + var eventArgs = new DropEventArgs(_currentDragData, droppedData); + Drop?.Invoke(this, eventArgs); + + // Send XdndFinished + SendXdndFinished(eventArgs.Handled); + + _currentDragData = null; + _dragSource = IntPtr.Zero; + + return true; + } + + private List GetTypeList(nint window) + { + var types = new List(); + + nint actualType; + int actualFormat; + nint nitems, bytesAfter; + nint data; + + int result = XGetWindowProperty(_display, window, _xdndTypeList, 0, 1024, false, + XA_ATOM, out actualType, out actualFormat, out nitems, out bytesAfter, out data); + + if (result == 0 && data != IntPtr.Zero) + { + for (int i = 0; i < (int)nitems; i++) + { + nint atom = Marshal.ReadIntPtr(data, i * IntPtr.Size); + types.Add(atom); + } + XFree(data); + } + + return types; + } + + private void SendXdndStatus(bool accepted, DragAction action) + { + var ev = new XClientMessageEvent + { + type = ClientMessage, + window = _dragSource, + message_type = _xdndStatus, + format = 32 + }; + + ev.data0 = _window; + ev.data1 = accepted ? 1 : 0; + ev.data2 = 0; // x, y of rectangle + ev.data3 = 0; // width, height of rectangle + ev.data4 = GetActionAtom(action); + + XSendEvent(_display, _dragSource, false, 0, ref ev); + XFlush(_display); + } + + private void SendXdndFinished(bool accepted) + { + var ev = new XClientMessageEvent + { + type = ClientMessage, + window = _dragSource, + message_type = _xdndFinished, + format = 32 + }; + + ev.data0 = _window; + ev.data1 = accepted ? 1 : 0; + ev.data2 = accepted ? _xdndActionCopy : IntPtr.Zero; + + XSendEvent(_display, _dragSource, false, 0, ref ev); + XFlush(_display); + } + + private string? RequestDropData(uint timestamp) + { + // Convert selection to get the data + nint targetType = _textPlain; + + // Check if text/uri-list is available + if (_currentDragData != null) + { + foreach (var type in _currentDragData.SupportedTypes) + { + if (type == _textUri) + { + targetType = _textUri; + break; + } + } + } + + // Request selection conversion + XConvertSelection(_display, _xdndSelection, targetType, _xdndSelection, _window, timestamp); + XFlush(_display); + + // In a real implementation, we would wait for SelectionNotify event + // and then get the data. For simplicity, we return null here. + // The actual data retrieval requires an event loop integration. + + return null; + } + + private DragAction GetDragAction(nint atom) + { + if (atom == _xdndActionCopy) return DragAction.Copy; + if (atom == _xdndActionMove) return DragAction.Move; + if (atom == _xdndActionLink) return DragAction.Link; + return DragAction.None; + } + + private nint GetActionAtom(DragAction action) + { + return action switch + { + DragAction.Copy => _xdndActionCopy, + DragAction.Move => _xdndActionMove, + DragAction.Link => _xdndActionLink, + _ => IntPtr.Zero + }; + } + + /// + /// Starts a drag operation. + /// + public void StartDrag(DragData data) + { + if (_isDragging) return; + + _isDragging = true; + _currentDragData = data; + + // Set the drag cursor and initiate the drag + // This requires integration with the X11 event loop + } + + /// + /// Cancels the current drag operation. + /// + public void CancelDrag() + { + _isDragging = false; + _currentDragData = null; + } + + public void Dispose() + { + if (_disposed) return; + _disposed = true; + } + + #region X11 Interop + + private const int ClientMessage = 33; + private const int PropModeReplace = 0; + private static readonly nint XA_ATOM = (nint)4; + + [StructLayout(LayoutKind.Sequential)] + private struct XClientMessageEvent + { + public int type; + public ulong serial; + public bool send_event; + public nint display; + public nint window; + public nint message_type; + public int format; + public nint data0; + public nint data1; + public nint data2; + public nint data3; + public nint data4; + } + + [DllImport("libX11.so.6")] + private static extern nint XInternAtom(nint display, string atomName, bool onlyIfExists); + + [DllImport("libX11.so.6")] + private static extern int XChangeProperty(nint display, nint window, nint property, nint type, + int format, int mode, ref int data, int nelements); + + [DllImport("libX11.so.6")] + private static extern int XGetWindowProperty(nint display, nint window, nint property, + long offset, long length, bool delete, nint reqType, + out nint actualType, out int actualFormat, out nint nitems, out nint bytesAfter, out nint data); + + [DllImport("libX11.so.6")] + private static extern int XSendEvent(nint display, nint window, bool propagate, long eventMask, ref XClientMessageEvent xevent); + + [DllImport("libX11.so.6")] + private static extern int XConvertSelection(nint display, nint selection, nint target, nint property, nint requestor, uint time); + + [DllImport("libX11.so.6")] + private static extern void XFree(nint ptr); + + [DllImport("libX11.so.6")] + private static extern void XFlush(nint display); + + #endregion +} + +/// +/// Contains data for a drag operation. +/// +public class DragData +{ + /// + /// Gets or sets the source window. + /// + public nint SourceWindow { get; set; } + + /// + /// Gets or sets the supported MIME types. + /// + public nint[] SupportedTypes { get; set; } = Array.Empty(); + + /// + /// Gets or sets the text data. + /// + public string? Text { get; set; } + + /// + /// Gets or sets the file paths. + /// + public string[]? FilePaths { get; set; } + + /// + /// Gets or sets custom data. + /// + public object? Data { get; set; } +} + +/// +/// Event args for drag events. +/// +public class DragEventArgs : EventArgs +{ + /// + /// Gets the drag data. + /// + public DragData Data { get; } + + /// + /// Gets the X coordinate. + /// + public int X { get; } + + /// + /// Gets the Y coordinate. + /// + public int Y { get; } + + /// + /// Gets or sets whether the drop is accepted. + /// + public bool Accepted { get; set; } + + /// + /// Gets or sets the allowed action. + /// + public DragAction AllowedAction { get; set; } + + /// + /// Gets or sets the accepted action. + /// + public DragAction AcceptedAction { get; set; } = DragAction.Copy; + + public DragEventArgs(DragData data, int x, int y) + { + Data = data; + X = x; + Y = y; + } +} + +/// +/// Event args for drop events. +/// +public class DropEventArgs : EventArgs +{ + /// + /// Gets the drag data. + /// + public DragData Data { get; } + + /// + /// Gets the dropped data as string. + /// + public string? DroppedData { get; } + + /// + /// Gets or sets whether the drop was handled. + /// + public bool Handled { get; set; } + + public DropEventArgs(DragData data, string? droppedData) + { + Data = data; + DroppedData = droppedData; + } +} + +/// +/// Drag action types. +/// +public enum DragAction +{ + None, + Copy, + Move, + Link } diff --git a/Services/DragEventArgs.cs b/Services/DragEventArgs.cs deleted file mode 100644 index 571d481..0000000 --- a/Services/DragEventArgs.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; - -namespace Microsoft.Maui.Platform.Linux.Services; - -public class DragEventArgs : EventArgs -{ - public DragData Data { get; } - - public int X { get; } - - public int Y { get; } - - public bool Accepted { get; set; } - - public DragAction AllowedAction { get; set; } - - public DragAction AcceptedAction { get; set; } = DragAction.Copy; - - public DragEventArgs(DragData data, int x, int y) - { - Data = data; - X = x; - Y = y; - } -} diff --git a/Services/DropEventArgs.cs b/Services/DropEventArgs.cs deleted file mode 100644 index 449e228..0000000 --- a/Services/DropEventArgs.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; - -namespace Microsoft.Maui.Platform.Linux.Services; - -public class DropEventArgs : EventArgs -{ - public DragData Data { get; } - - public string? DroppedData { get; } - - public bool Handled { get; set; } - - public DropEventArgs(DragData data, string? droppedData) - { - Data = data; - DroppedData = droppedData; - } -} diff --git a/Services/EmailService.cs b/Services/EmailService.cs index 4733fa9..1dd39c3 100644 --- a/Services/EmailService.cs +++ b/Services/EmailService.cs @@ -1,98 +1,113 @@ -using System; -using System.Collections.Generic; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using System.Diagnostics; -using System.Linq; using System.Text; -using System.Threading.Tasks; using Microsoft.Maui.ApplicationModel.Communication; namespace Microsoft.Maui.Platform.Linux.Services; +/// +/// Linux email implementation using mailto: URI. +/// public class EmailService : IEmail { - public bool IsComposeSupported => true; + public bool IsComposeSupported => true; - public async Task ComposeAsync() - { - await ComposeAsync(new EmailMessage()); - } + public async Task ComposeAsync() + { + await ComposeAsync(new EmailMessage()); + } - public async Task ComposeAsync(string subject, string body, params string[] to) - { - EmailMessage val = new EmailMessage - { - Subject = subject, - Body = body - }; - if (to != null && to.Length != 0) - { - val.To = new List(to); - } - await ComposeAsync(val); - } + public async Task ComposeAsync(string subject, string body, params string[] to) + { + var message = new EmailMessage + { + Subject = subject, + Body = body + }; - public async Task ComposeAsync(EmailMessage? message) - { - if (message == null) - { - throw new ArgumentNullException("message"); - } - string text = BuildMailtoUri(message); - try - { - ProcessStartInfo startInfo = new ProcessStartInfo - { - FileName = "xdg-open", - Arguments = "\"" + text + "\"", - UseShellExecute = false, - RedirectStandardOutput = true, - RedirectStandardError = true, - CreateNoWindow = true - }; - using Process process = Process.Start(startInfo); - if (process != null) - { - await process.WaitForExitAsync(); - } - } - catch (Exception innerException) - { - throw new InvalidOperationException("Failed to open email client", innerException); - } - } + if (to != null && to.Length > 0) + { + message.To = new List(to); + } - private static string BuildMailtoUri(EmailMessage? message) - { - StringBuilder stringBuilder = new StringBuilder("mailto:"); - List to = message.To; - if (to != null && to.Count > 0) - { - stringBuilder.Append(string.Join(",", message.To.Select(Uri.EscapeDataString))); - } - List list = new List(); - if (!string.IsNullOrEmpty(message.Subject)) - { - list.Add("subject=" + Uri.EscapeDataString(message.Subject)); - } - if (!string.IsNullOrEmpty(message.Body)) - { - list.Add("body=" + Uri.EscapeDataString(message.Body)); - } - List cc = message.Cc; - if (cc != null && cc.Count > 0) - { - list.Add("cc=" + string.Join(",", message.Cc.Select(Uri.EscapeDataString))); - } - List bcc = message.Bcc; - if (bcc != null && bcc.Count > 0) - { - list.Add("bcc=" + string.Join(",", message.Bcc.Select(Uri.EscapeDataString))); - } - if (list.Count > 0) - { - stringBuilder.Append('?'); - stringBuilder.Append(string.Join("&", list)); - } - return stringBuilder.ToString(); - } + await ComposeAsync(message); + } + + public async Task ComposeAsync(EmailMessage? message) + { + if (message == null) + throw new ArgumentNullException(nameof(message)); + + var mailto = BuildMailtoUri(message); + + try + { + var startInfo = new ProcessStartInfo + { + FileName = "xdg-open", + Arguments = $"\"{mailto}\"", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + + using var process = Process.Start(startInfo); + if (process != null) + { + await process.WaitForExitAsync(); + } + } + catch (Exception ex) + { + throw new InvalidOperationException("Failed to open email client", ex); + } + } + + private static string BuildMailtoUri(EmailMessage? message) + { + var sb = new StringBuilder("mailto:"); + + // Add recipients + if (message.To?.Count > 0) + { + sb.Append(string.Join(",", message.To.Select(Uri.EscapeDataString))); + } + + var queryParams = new List(); + + // Add subject + if (!string.IsNullOrEmpty(message.Subject)) + { + queryParams.Add($"subject={Uri.EscapeDataString(message.Subject)}"); + } + + // Add body + if (!string.IsNullOrEmpty(message.Body)) + { + queryParams.Add($"body={Uri.EscapeDataString(message.Body)}"); + } + + // Add CC + if (message.Cc?.Count > 0) + { + queryParams.Add($"cc={string.Join(",", message.Cc.Select(Uri.EscapeDataString))}"); + } + + // Add BCC + if (message.Bcc?.Count > 0) + { + queryParams.Add($"bcc={string.Join(",", message.Bcc.Select(Uri.EscapeDataString))}"); + } + + if (queryParams.Count > 0) + { + sb.Append('?'); + sb.Append(string.Join("&", queryParams)); + } + + return sb.ToString(); + } } diff --git a/Services/Fcitx5InputMethodService.cs b/Services/Fcitx5InputMethodService.cs deleted file mode 100644 index 5840f19..0000000 --- a/Services/Fcitx5InputMethodService.cs +++ /dev/null @@ -1,329 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Text.RegularExpressions; -using System.Threading.Tasks; - -namespace Microsoft.Maui.Platform.Linux.Services; - -public class Fcitx5InputMethodService : IInputMethodService, IDisposable -{ - private IInputContext? _currentContext; - - private string _preEditText = string.Empty; - - private int _preEditCursorPosition; - - private bool _isActive; - - private bool _disposed; - - private Process? _dBusMonitor; - - private string? _inputContextPath; - - public bool IsActive => _isActive; - - public string PreEditText => _preEditText; - - public int PreEditCursorPosition => _preEditCursorPosition; - - public event EventHandler? TextCommitted; - - public event EventHandler? PreEditChanged; - - public event EventHandler? PreEditEnded; - - public void Initialize(IntPtr windowHandle) - { - try - { - string text = RunDBusCommand("call --session --dest org.fcitx.Fcitx5 --object-path /org/freedesktop/portal/inputmethod --method org.fcitx.Fcitx.InputMethod1.CreateInputContext \"maui-linux\" \"\""); - if (!string.IsNullOrEmpty(text) && text.Contains("/")) - { - int num = text.IndexOf("'/"); - int num2 = text.IndexOf("'", num + 1); - if (num >= 0 && num2 > num) - { - _inputContextPath = text.Substring(num + 1, num2 - num - 1); - Console.WriteLine("Fcitx5InputMethodService: Created context at " + _inputContextPath); - StartMonitoring(); - } - } - else - { - Console.WriteLine("Fcitx5InputMethodService: Failed to create input context"); - } - } - catch (Exception ex) - { - Console.WriteLine("Fcitx5InputMethodService: Initialization failed - " + ex.Message); - } - } - - private void StartMonitoring() - { - if (string.IsNullOrEmpty(_inputContextPath)) - { - return; - } - Task.Run(async delegate - { - _ = 2; - try - { - ProcessStartInfo startInfo = new ProcessStartInfo - { - FileName = "dbus-monitor", - Arguments = "--session \"path='" + _inputContextPath + "'\"", - UseShellExecute = false, - RedirectStandardOutput = true, - CreateNoWindow = true - }; - _dBusMonitor = Process.Start(startInfo); - if (_dBusMonitor != null) - { - StreamReader reader = _dBusMonitor.StandardOutput; - while (!_disposed && !_dBusMonitor.HasExited) - { - string text = await reader.ReadLineAsync(); - if (text == null) - { - break; - } - if (text.Contains("CommitString")) - { - await ProcessCommitSignal(reader); - } - else if (text.Contains("UpdatePreedit")) - { - await ProcessPreeditSignal(reader); - } - } - } - } - catch (Exception ex) - { - Console.WriteLine("Fcitx5InputMethodService: Monitor error - " + ex.Message); - } - }); - } - - private async Task ProcessCommitSignal(StreamReader reader) - { - try - { - for (int i = 0; i < 5; i++) - { - string text = await reader.ReadLineAsync(); - if (text == null) - { - break; - } - if (text.Contains("string")) - { - Match match = Regex.Match(text, "string\\s+\"([^\"]*)\""); - if (match.Success) - { - string value = match.Groups[1].Value; - _preEditText = string.Empty; - _preEditCursorPosition = 0; - _isActive = false; - this.TextCommitted?.Invoke(this, new TextCommittedEventArgs(value)); - _currentContext?.OnTextCommitted(value); - break; - } - } - } - } - catch - { - } - } - - private async Task ProcessPreeditSignal(StreamReader reader) - { - try - { - for (int i = 0; i < 10; i++) - { - string text = await reader.ReadLineAsync(); - if (text == null) - { - break; - } - if (text.Contains("string")) - { - Match match = Regex.Match(text, "string\\s+\"([^\"]*)\""); - if (match.Success) - { - _preEditText = match.Groups[1].Value; - _isActive = !string.IsNullOrEmpty(_preEditText); - this.PreEditChanged?.Invoke(this, new PreEditChangedEventArgs(_preEditText, _preEditCursorPosition, new List())); - _currentContext?.OnPreEditChanged(_preEditText, _preEditCursorPosition); - break; - } - } - } - } - catch - { - } - } - - public void SetFocus(IInputContext? context) - { - _currentContext = context; - if (!string.IsNullOrEmpty(_inputContextPath)) - { - if (context != null) - { - RunDBusCommand($"call --session --dest org.fcitx.Fcitx5 --object-path {_inputContextPath} --method org.fcitx.Fcitx.InputContext1.FocusIn"); - } - else - { - RunDBusCommand($"call --session --dest org.fcitx.Fcitx5 --object-path {_inputContextPath} --method org.fcitx.Fcitx.InputContext1.FocusOut"); - } - } - } - - public void SetCursorLocation(int x, int y, int width, int height) - { - if (!string.IsNullOrEmpty(_inputContextPath)) - { - RunDBusCommand($"call --session --dest org.fcitx.Fcitx5 --object-path {_inputContextPath} --method org.fcitx.Fcitx.InputContext1.SetCursorRect {x} {y} {width} {height}"); - } - } - - public bool ProcessKeyEvent(uint keyCode, KeyModifiers modifiers, bool isKeyDown) - { - if (string.IsNullOrEmpty(_inputContextPath)) - { - return false; - } - uint num = ConvertModifiers(modifiers); - if (!isKeyDown) - { - num |= 0x40000000; - } - return RunDBusCommand($"call --session --dest org.fcitx.Fcitx5 --object-path {_inputContextPath} --method org.fcitx.Fcitx.InputContext1.ProcessKeyEvent {keyCode} {keyCode} {num} {(isKeyDown ? "true" : "false")} 0")?.Contains("true") ?? false; - } - - private uint ConvertModifiers(KeyModifiers modifiers) - { - uint num = 0u; - if (modifiers.HasFlag(KeyModifiers.Shift)) - { - num |= 1; - } - if (modifiers.HasFlag(KeyModifiers.CapsLock)) - { - num |= 2; - } - if (modifiers.HasFlag(KeyModifiers.Control)) - { - num |= 4; - } - if (modifiers.HasFlag(KeyModifiers.Alt)) - { - num |= 8; - } - if (modifiers.HasFlag(KeyModifiers.Super)) - { - num |= 0x40; - } - return num; - } - - public void Reset() - { - if (!string.IsNullOrEmpty(_inputContextPath)) - { - RunDBusCommand($"call --session --dest org.fcitx.Fcitx5 --object-path {_inputContextPath} --method org.fcitx.Fcitx.InputContext1.Reset"); - } - _preEditText = string.Empty; - _preEditCursorPosition = 0; - _isActive = false; - this.PreEditEnded?.Invoke(this, EventArgs.Empty); - _currentContext?.OnPreEditEnded(); - } - - public void Shutdown() - { - Dispose(); - } - - private string? RunDBusCommand(string args) - { - try - { - using Process process = Process.Start(new ProcessStartInfo - { - FileName = "gdbus", - Arguments = args, - UseShellExecute = false, - RedirectStandardOutput = true, - RedirectStandardError = true, - CreateNoWindow = true - }); - if (process == null) - { - return null; - } - string result = process.StandardOutput.ReadToEnd(); - process.WaitForExit(1000); - return result; - } - catch - { - return null; - } - } - - public void Dispose() - { - if (!_disposed) - { - _disposed = true; - try - { - _dBusMonitor?.Kill(); - _dBusMonitor?.Dispose(); - } - catch - { - } - if (!string.IsNullOrEmpty(_inputContextPath)) - { - RunDBusCommand($"call --session --dest org.fcitx.Fcitx5 --object-path {_inputContextPath} --method org.fcitx.Fcitx.InputContext1.Destroy"); - } - } - } - - public static bool IsAvailable() - { - try - { - using Process process = Process.Start(new ProcessStartInfo - { - FileName = "gdbus", - Arguments = "introspect --session --dest org.fcitx.Fcitx5 --object-path /org/freedesktop/portal/inputmethod", - UseShellExecute = false, - RedirectStandardOutput = true, - RedirectStandardError = true, - CreateNoWindow = true - }); - if (process == null) - { - return false; - } - process.WaitForExit(1000); - return process.ExitCode == 0; - } - catch - { - return false; - } - } -} diff --git a/Services/FileDialogResult.cs b/Services/FileDialogResult.cs deleted file mode 100644 index a1c550e..0000000 --- a/Services/FileDialogResult.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; - -namespace Microsoft.Maui.Platform.Linux.Services; - -public class FileDialogResult -{ - public bool Accepted { get; init; } - - public string[] SelectedFiles { get; init; } = Array.Empty(); - - public string? SelectedFile - { - get - { - if (SelectedFiles.Length == 0) - { - return null; - } - return SelectedFiles[0]; - } - } -} diff --git a/Services/FilePickerService.cs b/Services/FilePickerService.cs index 8e61b9d..76e7a6f 100644 --- a/Services/FilePickerService.cs +++ b/Services/FilePickerService.cs @@ -1,213 +1,212 @@ -using System; -using System.Collections.Generic; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using System.Diagnostics; -using System.IO; -using System.Linq; using System.Text; -using System.Threading.Tasks; using Microsoft.Maui.Storage; namespace Microsoft.Maui.Platform.Linux.Services; +/// +/// Linux file picker implementation using zenity or kdialog. +/// public class FilePickerService : IFilePicker { - private enum DialogTool - { - None, - Zenity, - Kdialog - } + private enum DialogTool + { + None, + Zenity, + Kdialog + } - private static DialogTool? _availableTool; + private static DialogTool? _availableTool; - private static DialogTool GetAvailableTool() - { - if (_availableTool.HasValue) - { - return _availableTool.Value; - } - if (IsToolAvailable("zenity")) - { - _availableTool = DialogTool.Zenity; - return DialogTool.Zenity; - } - if (IsToolAvailable("kdialog")) - { - _availableTool = DialogTool.Kdialog; - return DialogTool.Kdialog; - } - _availableTool = DialogTool.None; - return DialogTool.None; - } + private static DialogTool GetAvailableTool() + { + if (_availableTool.HasValue) + return _availableTool.Value; - private static bool IsToolAvailable(string tool) - { - try - { - using Process process = Process.Start(new ProcessStartInfo - { - FileName = "which", - Arguments = tool, - UseShellExecute = false, - RedirectStandardOutput = true, - CreateNoWindow = true - }); - process?.WaitForExit(1000); - return process != null && process.ExitCode == 0; - } - catch - { - return false; - } - } + // Check for zenity first (GNOME/GTK) + if (IsToolAvailable("zenity")) + { + _availableTool = DialogTool.Zenity; + return DialogTool.Zenity; + } - public Task PickAsync(PickOptions? options = null) - { - return PickInternalAsync(options, multiple: false); - } + // Check for kdialog (KDE) + if (IsToolAvailable("kdialog")) + { + _availableTool = DialogTool.Kdialog; + return DialogTool.Kdialog; + } - public Task> PickMultipleAsync(PickOptions? options = null) - { - return PickMultipleInternalAsync(options); - } + _availableTool = DialogTool.None; + return DialogTool.None; + } - private async Task PickInternalAsync(PickOptions? options, bool multiple) - { - return (await PickMultipleInternalAsync(options, multiple)).FirstOrDefault(); - } + private static bool IsToolAvailable(string tool) + { + try + { + var psi = new ProcessStartInfo + { + FileName = "which", + Arguments = tool, + UseShellExecute = false, + RedirectStandardOutput = true, + CreateNoWindow = true + }; - private Task> PickMultipleInternalAsync(PickOptions? options, bool multiple = true) - { - return Task.Run(delegate - { - DialogTool availableTool = GetAvailableTool(); - string arguments; - switch (availableTool) - { - case DialogTool.None: - { - Console.WriteLine("No file dialog available. Please enter file path:"); - string text = Console.ReadLine(); - if (!string.IsNullOrEmpty(text) && File.Exists(text)) - { - return (IEnumerable)(object)new LinuxFileResult[1] - { - new LinuxFileResult(text) - }; - } - return Array.Empty(); - } - case DialogTool.Zenity: - arguments = BuildZenityArguments(options, multiple); - break; - default: - arguments = BuildKdialogArguments(options, multiple); - break; - } - ProcessStartInfo startInfo = new ProcessStartInfo - { - FileName = ((availableTool == DialogTool.Zenity) ? "zenity" : "kdialog"), - Arguments = arguments, - UseShellExecute = false, - RedirectStandardOutput = true, - RedirectStandardError = true, - CreateNoWindow = true - }; - try - { - using Process process = Process.Start(startInfo); - if (process == null) - { - return Array.Empty(); - } - string text2 = process.StandardOutput.ReadToEnd().Trim(); - process.WaitForExit(); - if (process.ExitCode != 0 || string.IsNullOrEmpty(text2)) - { - return Array.Empty(); - } - char separator = ((availableTool == DialogTool.Zenity) ? '|' : '\n'); - return (from p in text2.Split(separator, StringSplitOptions.RemoveEmptyEntries).Where(File.Exists) - select (FileResult)(object)new LinuxFileResult(p)).ToArray(); - } - catch - { - return Array.Empty(); - } - }); - } + using var process = Process.Start(psi); + process?.WaitForExit(1000); + return process?.ExitCode == 0; + } + catch + { + return false; + } + } - private string BuildZenityArguments(PickOptions? options, bool multiple) - { - StringBuilder stringBuilder = new StringBuilder("--file-selection"); - if (multiple) - { - stringBuilder.Append(" --multiple --separator='|'"); - } - if (!string.IsNullOrEmpty((options != null) ? options.PickerTitle : null)) - { - StringBuilder stringBuilder2 = stringBuilder; - StringBuilder stringBuilder3 = stringBuilder2; - StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(11, 1, stringBuilder2); - handler.AppendLiteral(" --title=\""); - handler.AppendFormatted(EscapeArgument(options.PickerTitle)); - handler.AppendLiteral("\""); - stringBuilder3.Append(ref handler); - } - if (((options != null) ? options.FileTypes : null) != null) - { - foreach (string item in options.FileTypes.Value) - { - string value = (item.StartsWith(".") ? item : ("." + item)); - StringBuilder stringBuilder2 = stringBuilder; - StringBuilder stringBuilder4 = stringBuilder2; - StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(18, 1, stringBuilder2); - handler.AppendLiteral(" --file-filter='*"); - handler.AppendFormatted(value); - handler.AppendLiteral("'"); - stringBuilder4.Append(ref handler); - } - } - return stringBuilder.ToString(); - } + public Task PickAsync(PickOptions? options = null) + { + return PickInternalAsync(options, false); + } - private string BuildKdialogArguments(PickOptions? options, bool multiple) - { - StringBuilder stringBuilder = new StringBuilder("--getopenfilename"); - if (multiple) - { - stringBuilder.Insert(0, "--multiple "); - } - stringBuilder.Append(" ."); - if (((options != null) ? options.FileTypes : null) != null) - { - string value = string.Join(" ", options.FileTypes.Value.Select((string e) => (!e.StartsWith(".")) ? ("*." + e) : ("*" + e))); - if (!string.IsNullOrEmpty(value)) - { - StringBuilder stringBuilder2 = stringBuilder; - StringBuilder stringBuilder3 = stringBuilder2; - StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(3, 1, stringBuilder2); - handler.AppendLiteral(" \""); - handler.AppendFormatted(value); - handler.AppendLiteral("\""); - stringBuilder3.Append(ref handler); - } - } - if (!string.IsNullOrEmpty((options != null) ? options.PickerTitle : null)) - { - StringBuilder stringBuilder2 = stringBuilder; - StringBuilder stringBuilder4 = stringBuilder2; - StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(11, 1, stringBuilder2); - handler.AppendLiteral(" --title \""); - handler.AppendFormatted(EscapeArgument(options.PickerTitle)); - handler.AppendLiteral("\""); - stringBuilder4.Append(ref handler); - } - return stringBuilder.ToString(); - } + public Task> PickMultipleAsync(PickOptions? options = null) + { + return PickMultipleInternalAsync(options); + } - private static string EscapeArgument(string arg) - { - return arg.Replace("\"", "\\\"").Replace("'", "\\'"); - } + private async Task PickInternalAsync(PickOptions? options, bool multiple) + { + var results = await PickMultipleInternalAsync(options, multiple); + return results.FirstOrDefault(); + } + + private Task> PickMultipleInternalAsync(PickOptions? options, bool multiple = true) + { + return Task.Run>(() => + { + var tool = GetAvailableTool(); + if (tool == DialogTool.None) + { + // Fall back to console path input + Console.WriteLine("No file dialog available. Please enter file path:"); + var path = Console.ReadLine(); + if (!string.IsNullOrEmpty(path) && File.Exists(path)) + { + return new[] { new LinuxFileResult(path) }; + } + return Array.Empty(); + } + + string arguments; + if (tool == DialogTool.Zenity) + { + arguments = BuildZenityArguments(options, multiple); + } + else + { + arguments = BuildKdialogArguments(options, multiple); + } + + var psi = new ProcessStartInfo + { + FileName = tool == DialogTool.Zenity ? "zenity" : "kdialog", + Arguments = arguments, + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + + try + { + using var process = Process.Start(psi); + if (process == null) + return Array.Empty(); + + var output = process.StandardOutput.ReadToEnd().Trim(); + process.WaitForExit(); + + if (process.ExitCode != 0 || string.IsNullOrEmpty(output)) + return Array.Empty(); + + // Parse output (paths separated by | for zenity, newlines for kdialog) + var separator = tool == DialogTool.Zenity ? '|' : '\n'; + var paths = output.Split(separator, StringSplitOptions.RemoveEmptyEntries); + + return paths + .Where(File.Exists) + .Select(p => (FileResult)new LinuxFileResult(p)) + .ToArray(); + } + catch + { + return Array.Empty(); + } + }); + } + + private string BuildZenityArguments(PickOptions? options, bool multiple) + { + var sb = new StringBuilder("--file-selection"); + + if (multiple) + sb.Append(" --multiple --separator='|'"); + + if (!string.IsNullOrEmpty(options?.PickerTitle)) + sb.Append($" --title=\"{EscapeArgument(options.PickerTitle)}\""); + + if (options?.FileTypes != null) + { + foreach (var ext in options.FileTypes.Value) + { + var extension = ext.StartsWith(".") ? ext : $".{ext}"; + sb.Append($" --file-filter='*{extension}'"); + } + } + + return sb.ToString(); + } + + private string BuildKdialogArguments(PickOptions? options, bool multiple) + { + var sb = new StringBuilder("--getopenfilename"); + + if (multiple) + sb.Insert(0, "--multiple "); + + sb.Append(" ."); + + if (options?.FileTypes != null) + { + var extensions = string.Join(" ", options.FileTypes.Value.Select(e => + e.StartsWith(".") ? $"*{e}" : $"*.{e}")); + if (!string.IsNullOrEmpty(extensions)) + { + sb.Append($" \"{extensions}\""); + } + } + + if (!string.IsNullOrEmpty(options?.PickerTitle)) + sb.Append($" --title \"{EscapeArgument(options.PickerTitle)}\""); + + return sb.ToString(); + } + + private static string EscapeArgument(string arg) + { + return arg.Replace("\"", "\\\"").Replace("'", "\\'"); + } +} + +/// +/// Linux-specific FileResult implementation. +/// +internal class LinuxFileResult : FileResult +{ + public LinuxFileResult(string fullPath) : base(fullPath) + { + } } diff --git a/Services/FolderPickerOptions.cs b/Services/FolderPickerOptions.cs deleted file mode 100644 index e5d0d76..0000000 --- a/Services/FolderPickerOptions.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Microsoft.Maui.Platform.Linux.Services; - -public class FolderPickerOptions -{ - public string? Title { get; set; } - - public string? InitialDirectory { get; set; } -} diff --git a/Services/FolderPickerResult.cs b/Services/FolderPickerResult.cs deleted file mode 100644 index 5636fbc..0000000 --- a/Services/FolderPickerResult.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Microsoft.Maui.Platform.Linux.Services; - -public class FolderPickerResult -{ - public FolderResult? Folder { get; } - - public bool WasSuccessful => Folder != null; - - public FolderPickerResult(FolderResult? folder) - { - Folder = folder; - } -} diff --git a/Services/FolderPickerService.cs b/Services/FolderPickerService.cs index feb5e11..6e01dd9 100644 --- a/Services/FolderPickerService.cs +++ b/Services/FolderPickerService.cs @@ -1,121 +1,129 @@ -using System; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using System.Diagnostics; -using System.IO; -using System.Threading; -using System.Threading.Tasks; namespace Microsoft.Maui.Platform.Linux.Services; +/// +/// Linux folder picker utility using zenity or kdialog. +/// This is a standalone service as MAUI core does not define IFolderPicker. +/// public class FolderPickerService { - public async Task PickFolderAsync(string? initialDirectory = null, CancellationToken cancellationToken = default(CancellationToken)) - { - _ = 1; - try - { - string text = await TryZenityFolderPicker(initialDirectory, cancellationToken); - if (text != null) - { - return text; - } - text = await TryKdialogFolderPicker(initialDirectory, cancellationToken); - if (text != null) - { - return text; - } - return null; - } - catch (OperationCanceledException) - { - return null; - } - catch - { - return null; - } - } + public async Task PickFolderAsync(string? initialDirectory = null, CancellationToken cancellationToken = default) + { + try + { + // Try zenity first (GNOME) + var result = await TryZenityFolderPicker(initialDirectory, cancellationToken); + if (result != null) + { + return result; + } - private async Task TryZenityFolderPicker(string? initialDirectory, CancellationToken cancellationToken) - { - _ = 1; - try - { - string text = "--file-selection --directory"; - if (!string.IsNullOrEmpty(initialDirectory) && Directory.Exists(initialDirectory)) - { - text = text + " --filename=\"" + initialDirectory + "/\""; - } - ProcessStartInfo startInfo = new ProcessStartInfo - { - FileName = "zenity", - Arguments = text, - UseShellExecute = false, - RedirectStandardOutput = true, - RedirectStandardError = true, - CreateNoWindow = true - }; - using Process process = Process.Start(startInfo); - if (process == null) - { - return null; - } - string output = await process.StandardOutput.ReadToEndAsync(cancellationToken); - await process.WaitForExitAsync(cancellationToken); - if (process.ExitCode == 0 && !string.IsNullOrWhiteSpace(output)) - { - string text2 = output.Trim(); - if (Directory.Exists(text2)) - { - return text2; - } - } - return null; - } - catch - { - return null; - } - } + // Fall back to kdialog (KDE) + result = await TryKdialogFolderPicker(initialDirectory, cancellationToken); + if (result != null) + { + return result; + } - private async Task TryKdialogFolderPicker(string? initialDirectory, CancellationToken cancellationToken) - { - _ = 1; - try - { - string text = "--getexistingdirectory"; - if (!string.IsNullOrEmpty(initialDirectory) && Directory.Exists(initialDirectory)) - { - text = text + " \"" + initialDirectory + "\""; - } - ProcessStartInfo startInfo = new ProcessStartInfo - { - FileName = "kdialog", - Arguments = text, - UseShellExecute = false, - RedirectStandardOutput = true, - RedirectStandardError = true, - CreateNoWindow = true - }; - using Process process = Process.Start(startInfo); - if (process == null) - { - return null; - } - string output = await process.StandardOutput.ReadToEndAsync(cancellationToken); - await process.WaitForExitAsync(cancellationToken); - if (process.ExitCode == 0 && !string.IsNullOrWhiteSpace(output)) - { - string text2 = output.Trim(); - if (Directory.Exists(text2)) - { - return text2; - } - } - return null; - } - catch - { - return null; - } - } + return null; + } + catch (OperationCanceledException) + { + return null; + } + catch + { + return null; + } + } + + private async Task TryZenityFolderPicker(string? initialDirectory, CancellationToken cancellationToken) + { + try + { + var args = "--file-selection --directory"; + if (!string.IsNullOrEmpty(initialDirectory) && Directory.Exists(initialDirectory)) + { + args += $" --filename=\"{initialDirectory}/\""; + } + + var startInfo = new ProcessStartInfo + { + FileName = "zenity", + Arguments = args, + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + + using var process = Process.Start(startInfo); + if (process == null) return null; + + var output = await process.StandardOutput.ReadToEndAsync(cancellationToken); + await process.WaitForExitAsync(cancellationToken); + + if (process.ExitCode == 0 && !string.IsNullOrWhiteSpace(output)) + { + var path = output.Trim(); + if (Directory.Exists(path)) + { + return path; + } + } + + return null; + } + catch + { + return null; + } + } + + private async Task TryKdialogFolderPicker(string? initialDirectory, CancellationToken cancellationToken) + { + try + { + var args = "--getexistingdirectory"; + if (!string.IsNullOrEmpty(initialDirectory) && Directory.Exists(initialDirectory)) + { + args += $" \"{initialDirectory}\""; + } + + var startInfo = new ProcessStartInfo + { + FileName = "kdialog", + Arguments = args, + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + + using var process = Process.Start(startInfo); + if (process == null) return null; + + var output = await process.StandardOutput.ReadToEndAsync(cancellationToken); + await process.WaitForExitAsync(cancellationToken); + + if (process.ExitCode == 0 && !string.IsNullOrWhiteSpace(output)) + { + var path = output.Trim(); + if (Directory.Exists(path)) + { + return path; + } + } + + return null; + } + catch + { + return null; + } + } } diff --git a/Services/FolderResult.cs b/Services/FolderResult.cs deleted file mode 100644 index d882c51..0000000 --- a/Services/FolderResult.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.IO; - -namespace Microsoft.Maui.Platform.Linux.Services; - -public class FolderResult -{ - public string Path { get; } - - public string Name => System.IO.Path.GetFileName(Path) ?? Path; - - public FolderResult(string path) - { - Path = path; - } -} diff --git a/Services/FontFallbackManager.cs b/Services/FontFallbackManager.cs deleted file mode 100644 index 468eda3..0000000 --- a/Services/FontFallbackManager.cs +++ /dev/null @@ -1,186 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using SkiaSharp; - -namespace Microsoft.Maui.Platform.Linux.Services; - -public class FontFallbackManager -{ - private static FontFallbackManager? _instance; - - private static readonly object _lock = new object(); - - private readonly string[] _fallbackFonts = new string[27] - { - "Noto Sans", "DejaVu Sans", "Liberation Sans", "FreeSans", "Noto Color Emoji", "Noto Emoji", "Symbola", "Segoe UI Emoji", "Noto Sans CJK SC", "Noto Sans CJK TC", - "Noto Sans CJK JP", "Noto Sans CJK KR", "WenQuanYi Micro Hei", "WenQuanYi Zen Hei", "Droid Sans Fallback", "Noto Sans Arabic", "Noto Naskh Arabic", "DejaVu Sans", "Noto Sans Devanagari", "Noto Sans Tamil", - "Noto Sans Bengali", "Noto Sans Telugu", "Noto Sans Thai", "Loma", "Noto Sans Hebrew", "Sans", "sans-serif" - }; - - private readonly Dictionary _typefaceCache = new Dictionary(); - - private readonly Dictionary<(int codepoint, string preferredFont), SKTypeface?> _glyphCache = new Dictionary<(int, string), SKTypeface>(); - - public static FontFallbackManager Instance - { - get - { - if (_instance == null) - { - lock (_lock) - { - if (_instance == null) - { - _instance = new FontFallbackManager(); - } - } - } - return _instance; - } - } - - private FontFallbackManager() - { - foreach (string item in _fallbackFonts.Take(10)) - { - GetCachedTypeface(item); - } - } - - public SKTypeface GetTypefaceForCodepoint(int codepoint, SKTypeface preferred) - { - (int, string) key = (codepoint, preferred.FamilyName); - if (_glyphCache.TryGetValue(key, out SKTypeface value)) - { - return value ?? preferred; - } - if (TypefaceContainsGlyph(preferred, codepoint)) - { - _glyphCache[key] = preferred; - return preferred; - } - string[] fallbackFonts = _fallbackFonts; - foreach (string fontFamily in fallbackFonts) - { - SKTypeface cachedTypeface = GetCachedTypeface(fontFamily); - if (cachedTypeface != null && TypefaceContainsGlyph(cachedTypeface, codepoint)) - { - _glyphCache[key] = cachedTypeface; - return cachedTypeface; - } - } - _glyphCache[key] = null; - return preferred; - } - - public SKTypeface GetTypefaceForText(string text, SKTypeface preferred) - { - if (string.IsNullOrEmpty(text)) - { - return preferred; - } - foreach (Rune item in text.EnumerateRunes()) - { - if (item.Value > 127) - { - return GetTypefaceForCodepoint(item.Value, preferred); - } - } - return preferred; - } - - public List ShapeTextWithFallback(string text, SKTypeface preferred) - { - List list = new List(); - if (string.IsNullOrEmpty(text)) - { - return list; - } - _003CFontFallbackManager_003EFAC9D2911A2850E174CCA7662C668F37C2FBBA325CAF5C11AFE3FA59C16CC64ED__StringBuilder _003CFontFallbackManager_003EFAC9D2911A2850E174CCA7662C668F37C2FBBA325CAF5C11AFE3FA59C16CC64ED__StringBuilder2 = new _003CFontFallbackManager_003EFAC9D2911A2850E174CCA7662C668F37C2FBBA325CAF5C11AFE3FA59C16CC64ED__StringBuilder(); - SKTypeface val = null; - int startIndex = 0; - int num = 0; - foreach (Rune item in text.EnumerateRunes()) - { - SKTypeface typefaceForCodepoint = GetTypefaceForCodepoint(item.Value, preferred); - if (val == null) - { - val = typefaceForCodepoint; - } - else if (typefaceForCodepoint.FamilyName != val.FamilyName) - { - if (_003CFontFallbackManager_003EFAC9D2911A2850E174CCA7662C668F37C2FBBA325CAF5C11AFE3FA59C16CC64ED__StringBuilder2.Length > 0) - { - list.Add(new TextRun(_003CFontFallbackManager_003EFAC9D2911A2850E174CCA7662C668F37C2FBBA325CAF5C11AFE3FA59C16CC64ED__StringBuilder2.ToString(), val, startIndex)); - } - _003CFontFallbackManager_003EFAC9D2911A2850E174CCA7662C668F37C2FBBA325CAF5C11AFE3FA59C16CC64ED__StringBuilder2.Clear(); - val = typefaceForCodepoint; - startIndex = num; - } - _003CFontFallbackManager_003EFAC9D2911A2850E174CCA7662C668F37C2FBBA325CAF5C11AFE3FA59C16CC64ED__StringBuilder2.Append(item.ToString()); - num += item.Utf16SequenceLength; - } - if (_003CFontFallbackManager_003EFAC9D2911A2850E174CCA7662C668F37C2FBBA325CAF5C11AFE3FA59C16CC64ED__StringBuilder2.Length > 0 && val != null) - { - list.Add(new TextRun(_003CFontFallbackManager_003EFAC9D2911A2850E174CCA7662C668F37C2FBBA325CAF5C11AFE3FA59C16CC64ED__StringBuilder2.ToString(), val, startIndex)); - } - return list; - } - - public bool IsFontAvailable(string fontFamily) - { - SKTypeface cachedTypeface = GetCachedTypeface(fontFamily); - if (cachedTypeface != null) - { - return cachedTypeface.FamilyName.Equals(fontFamily, StringComparison.OrdinalIgnoreCase); - } - return false; - } - - public IEnumerable GetAvailableFallbackFonts() - { - string[] fallbackFonts = _fallbackFonts; - foreach (string text in fallbackFonts) - { - if (IsFontAvailable(text)) - { - yield return text; - } - } - } - - private SKTypeface? GetCachedTypeface(string fontFamily) - { - if (_typefaceCache.TryGetValue(fontFamily, out SKTypeface value)) - { - return value; - } - SKTypeface val = SKTypeface.FromFamilyName(fontFamily); - if (val != null && !val.FamilyName.Equals(fontFamily, StringComparison.OrdinalIgnoreCase)) - { - val = null; - } - _typefaceCache[fontFamily] = val; - return val; - } - - private bool TypefaceContainsGlyph(SKTypeface typeface, int codepoint) - { - //IL_0010: Unknown result type (might be due to invalid IL or missing references) - //IL_0016: Expected O, but got Unknown - SKFont val = new SKFont(typeface, 12f, 1f, 0f); - try - { - ushort[] array = new ushort[1]; - string text = char.ConvertFromUtf32(codepoint); - val.GetGlyphs(text, (Span)array); - return array[0] != 0; - } - finally - { - ((IDisposable)val)?.Dispose(); - } - } -} diff --git a/Services/GlobalHotkeyService.cs b/Services/GlobalHotkeyService.cs index 525738f..ef9a716 100644 --- a/Services/GlobalHotkeyService.cs +++ b/Services/GlobalHotkeyService.cs @@ -1,296 +1,393 @@ -using System; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using System.Collections.Concurrent; -using System.Linq; using System.Runtime.InteropServices; -using System.Threading; namespace Microsoft.Maui.Platform.Linux.Services; +/// +/// Provides global hotkey registration and handling using X11. +/// public class GlobalHotkeyService : IDisposable { - [StructLayout(LayoutKind.Explicit)] - private struct XEvent - { - [FieldOffset(0)] - public int type; + private nint _display; + private nint _rootWindow; + private readonly ConcurrentDictionary _registrations = new(); + private int _nextId = 1; + private bool _disposed; + private Thread? _eventThread; + private bool _isListening; - [FieldOffset(0)] - public XKeyEvent KeyEvent; - } + /// + /// Event raised when a registered hotkey is pressed. + /// + public event EventHandler? HotkeyPressed; - private struct XKeyEvent - { - public int type; + /// + /// Initializes the global hotkey service. + /// + public void Initialize() + { + _display = XOpenDisplay(IntPtr.Zero); + if (_display == IntPtr.Zero) + { + throw new InvalidOperationException("Failed to open X display"); + } - public ulong serial; + _rootWindow = XDefaultRootWindow(_display); - public bool send_event; + // Start listening for hotkeys in background + _isListening = true; + _eventThread = new Thread(ListenForHotkeys) + { + IsBackground = true, + Name = "GlobalHotkeyListener" + }; + _eventThread.Start(); + } - public IntPtr display; + /// + /// Registers a global hotkey. + /// + /// The key code. + /// The modifier keys. + /// A registration ID that can be used to unregister. + public int Register(HotkeyKey key, HotkeyModifiers modifiers) + { + if (_display == IntPtr.Zero) + { + throw new InvalidOperationException("Service not initialized"); + } - public IntPtr window; + int keyCode = XKeysymToKeycode(_display, (nint)key); + if (keyCode == 0) + { + throw new ArgumentException($"Invalid key: {key}"); + } - public IntPtr root; + uint modifierMask = GetModifierMask(modifiers); - public IntPtr subwindow; + // Register for all modifier combinations (with/without NumLock, CapsLock) + uint[] masks = GetModifierCombinations(modifierMask); - public ulong time; + foreach (var mask in masks) + { + int result = XGrabKey(_display, keyCode, mask, _rootWindow, true, GrabModeAsync, GrabModeAsync); + if (result == 0) + { + Console.WriteLine($"Failed to grab key {key} with modifiers {modifiers}"); + } + } - public int x; + int id = _nextId++; + _registrations[id] = new HotkeyRegistration + { + Id = id, + KeyCode = keyCode, + Modifiers = modifierMask, + Key = key, + ModifierKeys = modifiers + }; - public int y; + XFlush(_display); + return id; + } - public int x_root; + /// + /// Unregisters a global hotkey. + /// + /// The registration ID. + public void Unregister(int id) + { + if (_registrations.TryRemove(id, out var registration)) + { + uint[] masks = GetModifierCombinations(registration.Modifiers); - public int y_root; + foreach (var mask in masks) + { + XUngrabKey(_display, registration.KeyCode, mask, _rootWindow); + } - public uint state; + XFlush(_display); + } + } - public int keycode; + /// + /// Unregisters all global hotkeys. + /// + public void UnregisterAll() + { + foreach (var id in _registrations.Keys.ToList()) + { + Unregister(id); + } + } - public bool same_screen; - } + private void ListenForHotkeys() + { + while (_isListening && _display != IntPtr.Zero) + { + try + { + if (XPending(_display) > 0) + { + var xevent = new XEvent(); + XNextEvent(_display, ref xevent); - private class HotkeyRegistration - { - public int Id { get; set; } + if (xevent.type == KeyPress) + { + var keyEvent = xevent.KeyEvent; + ProcessKeyEvent(keyEvent.keycode, keyEvent.state); + } + } + else + { + Thread.Sleep(10); + } + } + catch (Exception ex) + { + Console.WriteLine($"GlobalHotkeyService error: {ex.Message}"); + } + } + } - public int KeyCode { get; set; } + private void ProcessKeyEvent(int keyCode, uint state) + { + // Remove NumLock and CapsLock from state for comparison + uint cleanState = state & ~(NumLockMask | CapsLockMask | ScrollLockMask); - public uint Modifiers { get; set; } + foreach (var registration in _registrations.Values) + { + if (registration.KeyCode == keyCode && + (registration.Modifiers == cleanState || + registration.Modifiers == (cleanState & ~Mod2Mask))) // Mod2 is often NumLock + { + OnHotkeyPressed(registration); + break; + } + } + } - public HotkeyKey Key { get; set; } + private void OnHotkeyPressed(HotkeyRegistration registration) + { + HotkeyPressed?.Invoke(this, new HotkeyEventArgs( + registration.Id, + registration.Key, + registration.ModifierKeys)); + } - public HotkeyModifiers ModifierKeys { get; set; } - } + private uint GetModifierMask(HotkeyModifiers modifiers) + { + uint mask = 0; + if (modifiers.HasFlag(HotkeyModifiers.Shift)) mask |= ShiftMask; + if (modifiers.HasFlag(HotkeyModifiers.Control)) mask |= ControlMask; + if (modifiers.HasFlag(HotkeyModifiers.Alt)) mask |= Mod1Mask; + if (modifiers.HasFlag(HotkeyModifiers.Super)) mask |= Mod4Mask; + return mask; + } - private IntPtr _display; + private uint[] GetModifierCombinations(uint baseMask) + { + // Include combinations with NumLock and CapsLock + return new uint[] + { + baseMask, + baseMask | NumLockMask, + baseMask | CapsLockMask, + baseMask | NumLockMask | CapsLockMask + }; + } - private IntPtr _rootWindow; + public void Dispose() + { + if (_disposed) return; + _disposed = true; - private readonly ConcurrentDictionary _registrations = new ConcurrentDictionary(); + _isListening = false; - private int _nextId = 1; + UnregisterAll(); - private bool _disposed; + if (_display != IntPtr.Zero) + { + XCloseDisplay(_display); + _display = IntPtr.Zero; + } + } - private Thread? _eventThread; + #region X11 Interop - private bool _isListening; + private const int KeyPress = 2; + private const int GrabModeAsync = 1; - private const int KeyPress = 2; + private const uint ShiftMask = 1 << 0; + private const uint LockMask = 1 << 1; // CapsLock + private const uint ControlMask = 1 << 2; + private const uint Mod1Mask = 1 << 3; // Alt + private const uint Mod2Mask = 1 << 4; // NumLock + private const uint Mod4Mask = 1 << 6; // Super - private const int GrabModeAsync = 1; + private const uint NumLockMask = Mod2Mask; + private const uint CapsLockMask = LockMask; + private const uint ScrollLockMask = 0; // Usually not used - private const uint ShiftMask = 1u; + [StructLayout(LayoutKind.Explicit)] + private struct XEvent + { + [FieldOffset(0)] public int type; + [FieldOffset(0)] public XKeyEvent KeyEvent; + } - private const uint LockMask = 2u; + [StructLayout(LayoutKind.Sequential)] + private struct XKeyEvent + { + public int type; + public ulong serial; + public bool send_event; + public nint display; + public nint window; + public nint root; + public nint subwindow; + public ulong time; + public int x, y; + public int x_root, y_root; + public uint state; + public int keycode; + public bool same_screen; + } - private const uint ControlMask = 4u; + [DllImport("libX11.so.6")] + private static extern nint XOpenDisplay(nint display); - private const uint Mod1Mask = 8u; + [DllImport("libX11.so.6")] + private static extern void XCloseDisplay(nint display); - private const uint Mod2Mask = 16u; + [DllImport("libX11.so.6")] + private static extern nint XDefaultRootWindow(nint display); - private const uint Mod4Mask = 64u; + [DllImport("libX11.so.6")] + private static extern int XKeysymToKeycode(nint display, nint keysym); - private const uint NumLockMask = 16u; + [DllImport("libX11.so.6")] + private static extern int XGrabKey(nint display, int keycode, uint modifiers, nint grabWindow, + bool ownerEvents, int pointerMode, int keyboardMode); - private const uint CapsLockMask = 2u; + [DllImport("libX11.so.6")] + private static extern int XUngrabKey(nint display, int keycode, uint modifiers, nint grabWindow); - private const uint ScrollLockMask = 0u; + [DllImport("libX11.so.6")] + private static extern int XPending(nint display); - public event EventHandler? HotkeyPressed; + [DllImport("libX11.so.6")] + private static extern int XNextEvent(nint display, ref XEvent xevent); - public void Initialize() - { - _display = XOpenDisplay(IntPtr.Zero); - if (_display == IntPtr.Zero) - { - throw new InvalidOperationException("Failed to open X display"); - } - _rootWindow = XDefaultRootWindow(_display); - _isListening = true; - _eventThread = new Thread(ListenForHotkeys) - { - IsBackground = true, - Name = "GlobalHotkeyListener" - }; - _eventThread.Start(); - } + [DllImport("libX11.so.6")] + private static extern void XFlush(nint display); - public int Register(HotkeyKey key, HotkeyModifiers modifiers) - { - if (_display == IntPtr.Zero) - { - throw new InvalidOperationException("Service not initialized"); - } - int num = XKeysymToKeycode(_display, (nint)key); - if (num == 0) - { - throw new ArgumentException($"Invalid key: {key}"); - } - uint modifierMask = GetModifierMask(modifiers); - uint[] modifierCombinations = GetModifierCombinations(modifierMask); - foreach (uint modifiers2 in modifierCombinations) - { - if (XGrabKey(_display, num, modifiers2, _rootWindow, ownerEvents: true, 1, 1) == 0) - { - Console.WriteLine($"Failed to grab key {key} with modifiers {modifiers}"); - } - } - int num2 = _nextId++; - _registrations[num2] = new HotkeyRegistration - { - Id = num2, - KeyCode = num, - Modifiers = modifierMask, - Key = key, - ModifierKeys = modifiers - }; - XFlush(_display); - return num2; - } + #endregion - public void Unregister(int id) - { - if (_registrations.TryRemove(id, out HotkeyRegistration value)) - { - uint[] modifierCombinations = GetModifierCombinations(value.Modifiers); - foreach (uint modifiers in modifierCombinations) - { - XUngrabKey(_display, value.KeyCode, modifiers, _rootWindow); - } - XFlush(_display); - } - } - - public void UnregisterAll() - { - foreach (int item in _registrations.Keys.ToList()) - { - Unregister(item); - } - } - - private void ListenForHotkeys() - { - while (_isListening && _display != IntPtr.Zero) - { - try - { - if (XPending(_display) > 0) - { - XEvent xevent = default(XEvent); - XNextEvent(_display, ref xevent); - if (xevent.type == 2) - { - XKeyEvent keyEvent = xevent.KeyEvent; - ProcessKeyEvent(keyEvent.keycode, keyEvent.state); - } - } - else - { - Thread.Sleep(10); - } - } - catch (Exception ex) - { - Console.WriteLine("GlobalHotkeyService error: " + ex.Message); - } - } - } - - private void ProcessKeyEvent(int keyCode, uint state) - { - uint num = state & 0xFFFFFFEDu; - foreach (HotkeyRegistration value in _registrations.Values) - { - if (value.KeyCode == keyCode && (value.Modifiers == num || value.Modifiers == (num & 0xFFFFFFEFu))) - { - OnHotkeyPressed(value); - break; - } - } - } - - private void OnHotkeyPressed(HotkeyRegistration registration) - { - this.HotkeyPressed?.Invoke(this, new HotkeyEventArgs(registration.Id, registration.Key, registration.ModifierKeys)); - } - - private uint GetModifierMask(HotkeyModifiers modifiers) - { - uint num = 0u; - if (modifiers.HasFlag(HotkeyModifiers.Shift)) - { - num |= 1; - } - if (modifiers.HasFlag(HotkeyModifiers.Control)) - { - num |= 4; - } - if (modifiers.HasFlag(HotkeyModifiers.Alt)) - { - num |= 8; - } - if (modifiers.HasFlag(HotkeyModifiers.Super)) - { - num |= 0x40; - } - return num; - } - - private uint[] GetModifierCombinations(uint baseMask) - { - return new uint[4] - { - baseMask, - baseMask | 0x10, - baseMask | 2, - baseMask | 0x10 | 2 - }; - } - - public void Dispose() - { - if (!_disposed) - { - _disposed = true; - _isListening = false; - UnregisterAll(); - if (_display != IntPtr.Zero) - { - XCloseDisplay(_display); - _display = IntPtr.Zero; - } - } - } - - [DllImport("libX11.so.6")] - private static extern IntPtr XOpenDisplay(IntPtr display); - - [DllImport("libX11.so.6")] - private static extern void XCloseDisplay(IntPtr display); - - [DllImport("libX11.so.6")] - private static extern IntPtr XDefaultRootWindow(IntPtr display); - - [DllImport("libX11.so.6")] - private static extern int XKeysymToKeycode(IntPtr display, IntPtr keysym); - - [DllImport("libX11.so.6")] - private static extern int XGrabKey(IntPtr display, int keycode, uint modifiers, IntPtr grabWindow, bool ownerEvents, int pointerMode, int keyboardMode); - - [DllImport("libX11.so.6")] - private static extern int XUngrabKey(IntPtr display, int keycode, uint modifiers, IntPtr grabWindow); - - [DllImport("libX11.so.6")] - private static extern int XPending(IntPtr display); - - [DllImport("libX11.so.6")] - private static extern int XNextEvent(IntPtr display, ref XEvent xevent); - - [DllImport("libX11.so.6")] - private static extern void XFlush(IntPtr display); + private class HotkeyRegistration + { + public int Id { get; set; } + public int KeyCode { get; set; } + public uint Modifiers { get; set; } + public HotkeyKey Key { get; set; } + public HotkeyModifiers ModifierKeys { get; set; } + } +} + +/// +/// Event args for hotkey pressed events. +/// +public class HotkeyEventArgs : EventArgs +{ + /// + /// Gets the registration ID. + /// + public int Id { get; } + + /// + /// Gets the key. + /// + public HotkeyKey Key { get; } + + /// + /// Gets the modifier keys. + /// + public HotkeyModifiers Modifiers { get; } + + public HotkeyEventArgs(int id, HotkeyKey key, HotkeyModifiers modifiers) + { + Id = id; + Key = key; + Modifiers = modifiers; + } +} + +/// +/// Hotkey modifier keys. +/// +[Flags] +public enum HotkeyModifiers +{ + None = 0, + Shift = 1 << 0, + Control = 1 << 1, + Alt = 1 << 2, + Super = 1 << 3 +} + +/// +/// Hotkey keys (X11 keysyms). +/// +public enum HotkeyKey : uint +{ + // Letters + A = 0x61, B = 0x62, C = 0x63, D = 0x64, E = 0x65, + F = 0x66, G = 0x67, H = 0x68, I = 0x69, J = 0x6A, + K = 0x6B, L = 0x6C, M = 0x6D, N = 0x6E, O = 0x6F, + P = 0x70, Q = 0x71, R = 0x72, S = 0x73, T = 0x74, + U = 0x75, V = 0x76, W = 0x77, X = 0x78, Y = 0x79, + Z = 0x7A, + + // Numbers + D0 = 0x30, D1 = 0x31, D2 = 0x32, D3 = 0x33, D4 = 0x34, + D5 = 0x35, D6 = 0x36, D7 = 0x37, D8 = 0x38, D9 = 0x39, + + // Function keys + F1 = 0xFFBE, F2 = 0xFFBF, F3 = 0xFFC0, F4 = 0xFFC1, + F5 = 0xFFC2, F6 = 0xFFC3, F7 = 0xFFC4, F8 = 0xFFC5, + F9 = 0xFFC6, F10 = 0xFFC7, F11 = 0xFFC8, F12 = 0xFFC9, + + // Special keys + Escape = 0xFF1B, + Tab = 0xFF09, + Return = 0xFF0D, + Space = 0x20, + BackSpace = 0xFF08, + Delete = 0xFFFF, + Insert = 0xFF63, + Home = 0xFF50, + End = 0xFF57, + PageUp = 0xFF55, + PageDown = 0xFF56, + + // Arrow keys + Left = 0xFF51, + Up = 0xFF52, + Right = 0xFF53, + Down = 0xFF54, + + // Media keys + AudioPlay = 0x1008FF14, + AudioStop = 0x1008FF15, + AudioPrev = 0x1008FF16, + AudioNext = 0x1008FF17, + AudioMute = 0x1008FF12, + AudioRaiseVolume = 0x1008FF13, + AudioLowerVolume = 0x1008FF11, + + // Print screen + Print = 0xFF61 } diff --git a/Services/Gtk4InteropService.cs b/Services/Gtk4InteropService.cs index 203408a..80079fb 100644 --- a/Services/Gtk4InteropService.cs +++ b/Services/Gtk4InteropService.cs @@ -1,632 +1,821 @@ -using System; -using System.Collections.Generic; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using System.Runtime.InteropServices; -using System.Threading; -using System.Threading.Tasks; namespace Microsoft.Maui.Platform.Linux.Services; +/// +/// GTK4 dialog response codes. +/// +public enum GtkResponseType +{ + None = -1, + Reject = -2, + Accept = -3, + DeleteEvent = -4, + Ok = -5, + Cancel = -6, + Close = -7, + Yes = -8, + No = -9, + Apply = -10, + Help = -11 +} + +/// +/// GTK4 message dialog types. +/// +public enum GtkMessageType +{ + Info = 0, + Warning = 1, + Question = 2, + Error = 3, + Other = 4 +} + +/// +/// GTK4 button layouts for dialogs. +/// +public enum GtkButtonsType +{ + None = 0, + Ok = 1, + Close = 2, + Cancel = 3, + YesNo = 4, + OkCancel = 5 +} + +/// +/// GTK4 file chooser actions. +/// +public enum GtkFileChooserAction +{ + Open = 0, + Save = 1, + SelectFolder = 2, + CreateFolder = 3 +} + +/// +/// Result from a file dialog. +/// +public class FileDialogResult +{ + public bool Accepted { get; init; } + public string[] SelectedFiles { get; init; } = Array.Empty(); + public string? SelectedFile => SelectedFiles.Length > 0 ? SelectedFiles[0] : null; +} + +/// +/// Result from a color dialog. +/// +public class ColorDialogResult +{ + public bool Accepted { get; init; } + public float Red { get; init; } + public float Green { get; init; } + public float Blue { get; init; } + public float Alpha { get; init; } +} + +/// +/// GTK4 interop layer for native Linux dialogs. +/// Provides native file pickers, message boxes, and color choosers. +/// public class Gtk4InteropService : IDisposable { - private struct GdkRGBA - { - public float Red; + #region GTK4 Native Interop - public float Green; + private const string LibGtk4 = "libgtk-4.so.1"; + private const string LibGio = "libgio-2.0.so.0"; + private const string LibGlib = "libglib-2.0.so.0"; + private const string LibGObject = "libgobject-2.0.so.0"; - public float Blue; + // GTK initialization + [DllImport(LibGtk4)] + private static extern bool gtk_init_check(); - public float Alpha; - } + [DllImport(LibGtk4)] + private static extern bool gtk_is_initialized(); - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - private delegate void GAsyncReadyCallback(IntPtr sourceObject, IntPtr result, IntPtr userData); + // Main loop + [DllImport(LibGtk4)] + private static extern IntPtr g_main_context_default(); - private const string LibGtk4 = "libgtk-4.so.1"; + [DllImport(LibGtk4)] + private static extern bool g_main_context_iteration(IntPtr context, bool mayBlock); - private const string LibGio = "libgio-2.0.so.0"; + [DllImport(LibGlib)] + private static extern void g_free(IntPtr mem); - private const string LibGlib = "libglib-2.0.so.0"; + // GObject + [DllImport(LibGObject)] + private static extern void g_object_unref(IntPtr obj); - private const string LibGObject = "libgobject-2.0.so.0"; + [DllImport(LibGObject)] + private static extern void g_object_ref(IntPtr obj); - private const string LibGtk3 = "libgtk-3.so.0"; + // Window + [DllImport(LibGtk4)] + private static extern IntPtr gtk_window_new(); - private bool _initialized; + [DllImport(LibGtk4)] + private static extern void gtk_window_set_title(IntPtr window, [MarshalAs(UnmanagedType.LPStr)] string title); - private bool _useGtk4; + [DllImport(LibGtk4)] + private static extern void gtk_window_set_modal(IntPtr window, bool modal); - private bool _disposed; + [DllImport(LibGtk4)] + private static extern void gtk_window_set_transient_for(IntPtr window, IntPtr parent); - private readonly object _lock = new object(); + [DllImport(LibGtk4)] + private static extern void gtk_window_destroy(IntPtr window); - private GAsyncReadyCallback? _currentCallback; + [DllImport(LibGtk4)] + private static extern void gtk_window_present(IntPtr window); - private TaskCompletionSource? _fileDialogTcs; + [DllImport(LibGtk4)] + private static extern void gtk_window_close(IntPtr window); - private TaskCompletionSource? _colorDialogTcs; + // Widget + [DllImport(LibGtk4)] + private static extern void gtk_widget_show(IntPtr widget); - private IntPtr _currentDialog; + [DllImport(LibGtk4)] + private static extern void gtk_widget_hide(IntPtr widget); - public bool IsInitialized => _initialized; + [DllImport(LibGtk4)] + private static extern void gtk_widget_set_visible(IntPtr widget, bool visible); - public bool IsGtk4 => _useGtk4; + [DllImport(LibGtk4)] + private static extern bool gtk_widget_get_visible(IntPtr widget); - [DllImport("libgtk-4.so.1")] - private static extern bool gtk_init_check(); + // Alert Dialog (GTK4) + [DllImport(LibGtk4)] + private static extern IntPtr gtk_alert_dialog_new([MarshalAs(UnmanagedType.LPStr)] string format); - [DllImport("libgtk-4.so.1")] - private static extern bool gtk_is_initialized(); + [DllImport(LibGtk4)] + private static extern void gtk_alert_dialog_set_message(IntPtr dialog, [MarshalAs(UnmanagedType.LPStr)] string message); - [DllImport("libgtk-4.so.1")] - private static extern IntPtr g_main_context_default(); + [DllImport(LibGtk4)] + private static extern void gtk_alert_dialog_set_detail(IntPtr dialog, [MarshalAs(UnmanagedType.LPStr)] string detail); - [DllImport("libgtk-4.so.1")] - private static extern bool g_main_context_iteration(IntPtr context, bool mayBlock); + [DllImport(LibGtk4)] + private static extern void gtk_alert_dialog_set_buttons(IntPtr dialog, string[] labels); - [DllImport("libglib-2.0.so.0")] - private static extern void g_free(IntPtr mem); + [DllImport(LibGtk4)] + private static extern void gtk_alert_dialog_set_cancel_button(IntPtr dialog, int button); - [DllImport("libgobject-2.0.so.0")] - private static extern void g_object_unref(IntPtr obj); + [DllImport(LibGtk4)] + private static extern void gtk_alert_dialog_set_default_button(IntPtr dialog, int button); - [DllImport("libgobject-2.0.so.0")] - private static extern void g_object_ref(IntPtr obj); + [DllImport(LibGtk4)] + private static extern void gtk_alert_dialog_show(IntPtr dialog, IntPtr parent); - [DllImport("libgtk-4.so.1")] - private static extern IntPtr gtk_window_new(); + // File Dialog (GTK4) + [DllImport(LibGtk4)] + private static extern IntPtr gtk_file_dialog_new(); - [DllImport("libgtk-4.so.1")] - private static extern void gtk_window_set_title(IntPtr window, [MarshalAs(UnmanagedType.LPStr)] string title); + [DllImport(LibGtk4)] + private static extern void gtk_file_dialog_set_title(IntPtr dialog, [MarshalAs(UnmanagedType.LPStr)] string title); - [DllImport("libgtk-4.so.1")] - private static extern void gtk_window_set_modal(IntPtr window, bool modal); + [DllImport(LibGtk4)] + private static extern void gtk_file_dialog_set_modal(IntPtr dialog, bool modal); - [DllImport("libgtk-4.so.1")] - private static extern void gtk_window_set_transient_for(IntPtr window, IntPtr parent); + [DllImport(LibGtk4)] + private static extern void gtk_file_dialog_set_accept_label(IntPtr dialog, [MarshalAs(UnmanagedType.LPStr)] string label); - [DllImport("libgtk-4.so.1")] - private static extern void gtk_window_destroy(IntPtr window); + [DllImport(LibGtk4)] + private static extern void gtk_file_dialog_open(IntPtr dialog, IntPtr parent, IntPtr cancellable, GAsyncReadyCallback callback, IntPtr userData); - [DllImport("libgtk-4.so.1")] - private static extern void gtk_window_present(IntPtr window); + [DllImport(LibGtk4)] + private static extern IntPtr gtk_file_dialog_open_finish(IntPtr dialog, IntPtr result, out IntPtr error); - [DllImport("libgtk-4.so.1")] - private static extern void gtk_window_close(IntPtr window); + [DllImport(LibGtk4)] + private static extern void gtk_file_dialog_save(IntPtr dialog, IntPtr parent, IntPtr cancellable, GAsyncReadyCallback callback, IntPtr userData); - [DllImport("libgtk-4.so.1")] - private static extern void gtk_widget_show(IntPtr widget); + [DllImport(LibGtk4)] + private static extern IntPtr gtk_file_dialog_save_finish(IntPtr dialog, IntPtr result, out IntPtr error); - [DllImport("libgtk-4.so.1")] - private static extern void gtk_widget_hide(IntPtr widget); + [DllImport(LibGtk4)] + private static extern void gtk_file_dialog_select_folder(IntPtr dialog, IntPtr parent, IntPtr cancellable, GAsyncReadyCallback callback, IntPtr userData); - [DllImport("libgtk-4.so.1")] - private static extern void gtk_widget_set_visible(IntPtr widget, bool visible); + [DllImport(LibGtk4)] + private static extern IntPtr gtk_file_dialog_select_folder_finish(IntPtr dialog, IntPtr result, out IntPtr error); - [DllImport("libgtk-4.so.1")] - private static extern bool gtk_widget_get_visible(IntPtr widget); + [DllImport(LibGtk4)] + private static extern void gtk_file_dialog_open_multiple(IntPtr dialog, IntPtr parent, IntPtr cancellable, GAsyncReadyCallback callback, IntPtr userData); - [DllImport("libgtk-4.so.1")] - private static extern IntPtr gtk_alert_dialog_new([MarshalAs(UnmanagedType.LPStr)] string format); + [DllImport(LibGtk4)] + private static extern IntPtr gtk_file_dialog_open_multiple_finish(IntPtr dialog, IntPtr result, out IntPtr error); - [DllImport("libgtk-4.so.1")] - private static extern void gtk_alert_dialog_set_message(IntPtr dialog, [MarshalAs(UnmanagedType.LPStr)] string message); + // File filters + [DllImport(LibGtk4)] + private static extern IntPtr gtk_file_filter_new(); - [DllImport("libgtk-4.so.1")] - private static extern void gtk_alert_dialog_set_detail(IntPtr dialog, [MarshalAs(UnmanagedType.LPStr)] string detail); + [DllImport(LibGtk4)] + private static extern void gtk_file_filter_set_name(IntPtr filter, [MarshalAs(UnmanagedType.LPStr)] string name); - [DllImport("libgtk-4.so.1")] - private static extern void gtk_alert_dialog_set_buttons(IntPtr dialog, string[] labels); + [DllImport(LibGtk4)] + private static extern void gtk_file_filter_add_pattern(IntPtr filter, [MarshalAs(UnmanagedType.LPStr)] string pattern); - [DllImport("libgtk-4.so.1")] - private static extern void gtk_alert_dialog_set_cancel_button(IntPtr dialog, int button); + [DllImport(LibGtk4)] + private static extern void gtk_file_filter_add_mime_type(IntPtr filter, [MarshalAs(UnmanagedType.LPStr)] string mimeType); - [DllImport("libgtk-4.so.1")] - private static extern void gtk_alert_dialog_set_default_button(IntPtr dialog, int button); + [DllImport(LibGtk4)] + private static extern void gtk_file_dialog_set_default_filter(IntPtr dialog, IntPtr filter); - [DllImport("libgtk-4.so.1")] - private static extern void gtk_alert_dialog_show(IntPtr dialog, IntPtr parent); + // GFile + [DllImport(LibGio)] + private static extern IntPtr g_file_get_path(IntPtr file); - [DllImport("libgtk-4.so.1")] - private static extern IntPtr gtk_file_dialog_new(); + // GListModel for multiple files + [DllImport(LibGio)] + private static extern uint g_list_model_get_n_items(IntPtr list); - [DllImport("libgtk-4.so.1")] - private static extern void gtk_file_dialog_set_title(IntPtr dialog, [MarshalAs(UnmanagedType.LPStr)] string title); + [DllImport(LibGio)] + private static extern IntPtr g_list_model_get_item(IntPtr list, uint position); - [DllImport("libgtk-4.so.1")] - private static extern void gtk_file_dialog_set_modal(IntPtr dialog, bool modal); + // Color Dialog (GTK4) + [DllImport(LibGtk4)] + private static extern IntPtr gtk_color_dialog_new(); - [DllImport("libgtk-4.so.1")] - private static extern void gtk_file_dialog_set_accept_label(IntPtr dialog, [MarshalAs(UnmanagedType.LPStr)] string label); + [DllImport(LibGtk4)] + private static extern void gtk_color_dialog_set_title(IntPtr dialog, [MarshalAs(UnmanagedType.LPStr)] string title); - [DllImport("libgtk-4.so.1")] - private static extern void gtk_file_dialog_open(IntPtr dialog, IntPtr parent, IntPtr cancellable, GAsyncReadyCallback callback, IntPtr userData); + [DllImport(LibGtk4)] + private static extern void gtk_color_dialog_set_modal(IntPtr dialog, bool modal); - [DllImport("libgtk-4.so.1")] - private static extern IntPtr gtk_file_dialog_open_finish(IntPtr dialog, IntPtr result, out IntPtr error); + [DllImport(LibGtk4)] + private static extern void gtk_color_dialog_set_with_alpha(IntPtr dialog, bool withAlpha); - [DllImport("libgtk-4.so.1")] - private static extern void gtk_file_dialog_save(IntPtr dialog, IntPtr parent, IntPtr cancellable, GAsyncReadyCallback callback, IntPtr userData); + [DllImport(LibGtk4)] + private static extern void gtk_color_dialog_choose_rgba(IntPtr dialog, IntPtr parent, IntPtr initialColor, IntPtr cancellable, GAsyncReadyCallback callback, IntPtr userData); - [DllImport("libgtk-4.so.1")] - private static extern IntPtr gtk_file_dialog_save_finish(IntPtr dialog, IntPtr result, out IntPtr error); + [DllImport(LibGtk4)] + private static extern IntPtr gtk_color_dialog_choose_rgba_finish(IntPtr dialog, IntPtr result, out IntPtr error); - [DllImport("libgtk-4.so.1")] - private static extern void gtk_file_dialog_select_folder(IntPtr dialog, IntPtr parent, IntPtr cancellable, GAsyncReadyCallback callback, IntPtr userData); + // GdkRGBA + [StructLayout(LayoutKind.Sequential)] + private struct GdkRGBA + { + public float Red; + public float Green; + public float Blue; + public float Alpha; + } - [DllImport("libgtk-4.so.1")] - private static extern IntPtr gtk_file_dialog_select_folder_finish(IntPtr dialog, IntPtr result, out IntPtr error); + // Async callback delegate + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate void GAsyncReadyCallback(IntPtr sourceObject, IntPtr result, IntPtr userData); - [DllImport("libgtk-4.so.1")] - private static extern void gtk_file_dialog_open_multiple(IntPtr dialog, IntPtr parent, IntPtr cancellable, GAsyncReadyCallback callback, IntPtr userData); + // Legacy GTK3 fallbacks + private const string LibGtk3 = "libgtk-3.so.0"; - [DllImport("libgtk-4.so.1")] - private static extern IntPtr gtk_file_dialog_open_multiple_finish(IntPtr dialog, IntPtr result, out IntPtr error); + [DllImport(LibGtk3, EntryPoint = "gtk_init_check")] + private static extern bool gtk3_init_check(ref int argc, ref IntPtr argv); - [DllImport("libgtk-4.so.1")] - private static extern IntPtr gtk_file_filter_new(); - - [DllImport("libgtk-4.so.1")] - private static extern void gtk_file_filter_set_name(IntPtr filter, [MarshalAs(UnmanagedType.LPStr)] string name); - - [DllImport("libgtk-4.so.1")] - private static extern void gtk_file_filter_add_pattern(IntPtr filter, [MarshalAs(UnmanagedType.LPStr)] string pattern); - - [DllImport("libgtk-4.so.1")] - private static extern void gtk_file_filter_add_mime_type(IntPtr filter, [MarshalAs(UnmanagedType.LPStr)] string mimeType); - - [DllImport("libgtk-4.so.1")] - private static extern void gtk_file_dialog_set_default_filter(IntPtr dialog, IntPtr filter); - - [DllImport("libgio-2.0.so.0")] - private static extern IntPtr g_file_get_path(IntPtr file); - - [DllImport("libgio-2.0.so.0")] - private static extern uint g_list_model_get_n_items(IntPtr list); - - [DllImport("libgio-2.0.so.0")] - private static extern IntPtr g_list_model_get_item(IntPtr list, uint position); - - [DllImport("libgtk-4.so.1")] - private static extern IntPtr gtk_color_dialog_new(); - - [DllImport("libgtk-4.so.1")] - private static extern void gtk_color_dialog_set_title(IntPtr dialog, [MarshalAs(UnmanagedType.LPStr)] string title); - - [DllImport("libgtk-4.so.1")] - private static extern void gtk_color_dialog_set_modal(IntPtr dialog, bool modal); - - [DllImport("libgtk-4.so.1")] - private static extern void gtk_color_dialog_set_with_alpha(IntPtr dialog, bool withAlpha); - - [DllImport("libgtk-4.so.1")] - private static extern void gtk_color_dialog_choose_rgba(IntPtr dialog, IntPtr parent, IntPtr initialColor, IntPtr cancellable, GAsyncReadyCallback callback, IntPtr userData); - - [DllImport("libgtk-4.so.1")] - private static extern IntPtr gtk_color_dialog_choose_rgba_finish(IntPtr dialog, IntPtr result, out IntPtr error); - - [DllImport("libgtk-3.so.0", EntryPoint = "gtk_init_check")] - private static extern bool gtk3_init_check(ref int argc, ref IntPtr argv); - - [DllImport("libgtk-3.so.0", EntryPoint = "gtk_file_chooser_dialog_new")] - private static extern IntPtr gtk3_file_chooser_dialog_new([MarshalAs(UnmanagedType.LPStr)] string title, IntPtr parent, int action, [MarshalAs(UnmanagedType.LPStr)] string firstButtonText, int firstButtonResponse, [MarshalAs(UnmanagedType.LPStr)] string secondButtonText, int secondButtonResponse, IntPtr terminator); - - [DllImport("libgtk-3.so.0", EntryPoint = "gtk_dialog_run")] - private static extern int gtk3_dialog_run(IntPtr dialog); - - [DllImport("libgtk-3.so.0", EntryPoint = "gtk_widget_destroy")] - private static extern void gtk3_widget_destroy(IntPtr widget); - - [DllImport("libgtk-3.so.0", EntryPoint = "gtk_file_chooser_get_filename")] - private static extern IntPtr gtk3_file_chooser_get_filename(IntPtr chooser); - - [DllImport("libgtk-3.so.0", EntryPoint = "gtk_file_chooser_get_filenames")] - private static extern IntPtr gtk3_file_chooser_get_filenames(IntPtr chooser); - - [DllImport("libgtk-3.so.0", EntryPoint = "gtk_file_chooser_set_select_multiple")] - private static extern void gtk3_file_chooser_set_select_multiple(IntPtr chooser, bool selectMultiple); - - [DllImport("libgtk-3.so.0", EntryPoint = "gtk_message_dialog_new")] - private static extern IntPtr gtk3_message_dialog_new(IntPtr parent, int flags, int type, int buttons, [MarshalAs(UnmanagedType.LPStr)] string message); - - [DllImport("libglib-2.0.so.0")] - private static extern uint g_slist_length(IntPtr list); - - [DllImport("libglib-2.0.so.0")] - private static extern IntPtr g_slist_nth_data(IntPtr list, uint n); - - [DllImport("libglib-2.0.so.0")] - private static extern void g_slist_free(IntPtr list); - - public bool Initialize() - { - if (_initialized) - { - return true; - } - lock (_lock) - { - if (_initialized) - { - return true; - } - try - { - if (gtk_init_check()) - { - _useGtk4 = true; - _initialized = true; - Console.WriteLine("[GTK4] Initialized GTK4"); - return true; - } - } - catch (DllNotFoundException) - { - Console.WriteLine("[GTK4] GTK4 not found, trying GTK3"); - } - catch (Exception ex2) - { - Console.WriteLine("[GTK4] GTK4 init failed: " + ex2.Message); - } - try - { - int argc = 0; - IntPtr argv = IntPtr.Zero; - if (gtk3_init_check(ref argc, ref argv)) - { - _useGtk4 = false; - _initialized = true; - Console.WriteLine("[GTK4] Initialized GTK3 (fallback)"); - return true; - } - } - catch (DllNotFoundException) - { - Console.WriteLine("[GTK4] GTK3 not found"); - } - catch (Exception ex4) - { - Console.WriteLine("[GTK4] GTK3 init failed: " + ex4.Message); - } - return false; - } - } - - public void ShowAlert(string title, string message, GtkMessageType type = GtkMessageType.Info) - { - if (EnsureInitialized()) - { - if (_useGtk4) - { - IntPtr intPtr = gtk_alert_dialog_new(title); - gtk_alert_dialog_set_detail(intPtr, message); - string[] labels = new string[1] { "OK" }; - gtk_alert_dialog_set_buttons(intPtr, labels); - gtk_alert_dialog_show(intPtr, IntPtr.Zero); - g_object_unref(intPtr); - } - else - { - IntPtr intPtr2 = gtk3_message_dialog_new(IntPtr.Zero, 1, (int)type, 1, message); - gtk3_dialog_run(intPtr2); - gtk3_widget_destroy(intPtr2); - } - ProcessPendingEvents(); - } - } - - public bool ShowConfirmation(string title, string message) - { - if (!EnsureInitialized()) - { - return false; - } - if (_useGtk4) - { - IntPtr intPtr = gtk_alert_dialog_new(title); - gtk_alert_dialog_set_detail(intPtr, message); - string[] labels = new string[2] { "No", "Yes" }; - gtk_alert_dialog_set_buttons(intPtr, labels); - gtk_alert_dialog_set_default_button(intPtr, 1); - gtk_alert_dialog_set_cancel_button(intPtr, 0); - gtk_alert_dialog_show(intPtr, IntPtr.Zero); - g_object_unref(intPtr); - return true; - } - IntPtr intPtr2 = gtk3_message_dialog_new(IntPtr.Zero, 1, 2, 4, message); - int num = gtk3_dialog_run(intPtr2); - gtk3_widget_destroy(intPtr2); - ProcessPendingEvents(); - return num == -8; - } - - public FileDialogResult ShowOpenFileDialog(string title = "Open File", string? initialFolder = null, bool allowMultiple = false, params (string Name, string Pattern)[] filters) - { - if (!EnsureInitialized()) - { - return new FileDialogResult - { - Accepted = false - }; - } - if (_useGtk4) - { - return ShowGtk4FileDialog(title, GtkFileChooserAction.Open, allowMultiple, filters); - } - return ShowGtk3FileDialog(title, 0, allowMultiple, filters); - } - - public FileDialogResult ShowSaveFileDialog(string title = "Save File", string? suggestedName = null, params (string Name, string Pattern)[] filters) - { - if (!EnsureInitialized()) - { - return new FileDialogResult - { - Accepted = false - }; - } - if (_useGtk4) - { - return ShowGtk4FileDialog(title, GtkFileChooserAction.Save, allowMultiple: false, filters); - } - return ShowGtk3FileDialog(title, 1, allowMultiple: false, filters); - } - - public FileDialogResult ShowFolderDialog(string title = "Select Folder") - { - if (!EnsureInitialized()) - { - return new FileDialogResult - { - Accepted = false - }; - } - if (_useGtk4) - { - return ShowGtk4FileDialog(title, GtkFileChooserAction.SelectFolder, allowMultiple: false, Array.Empty<(string, string)>()); - } - return ShowGtk3FileDialog(title, 2, allowMultiple: false, Array.Empty<(string, string)>()); - } - - private FileDialogResult ShowGtk4FileDialog(string title, GtkFileChooserAction action, bool allowMultiple, (string Name, string Pattern)[] filters) - { - IntPtr dialog = gtk_file_dialog_new(); - gtk_file_dialog_set_title(dialog, title); - gtk_file_dialog_set_modal(dialog, modal: true); - if (filters.Length != 0) - { - IntPtr filter = gtk_file_filter_new(); - gtk_file_filter_set_name(filter, filters[0].Name); - gtk_file_filter_add_pattern(filter, filters[0].Pattern); - gtk_file_dialog_set_default_filter(dialog, filter); - } - _fileDialogTcs = new TaskCompletionSource(); - _currentDialog = dialog; - _currentCallback = delegate(IntPtr source, IntPtr result, IntPtr userData) - { - IntPtr error = IntPtr.Zero; - IntPtr intPtr = IntPtr.Zero; - try - { - if (action == GtkFileChooserAction.Open && !allowMultiple) - { - intPtr = gtk_file_dialog_open_finish(dialog, result, out error); - } - else if (action == GtkFileChooserAction.Save) - { - intPtr = gtk_file_dialog_save_finish(dialog, result, out error); - } - else if (action == GtkFileChooserAction.SelectFolder) - { - intPtr = gtk_file_dialog_select_folder_finish(dialog, result, out error); - } - if (intPtr != IntPtr.Zero && error == IntPtr.Zero) - { - IntPtr intPtr2 = g_file_get_path(intPtr); - string text = Marshal.PtrToStringUTF8(intPtr2) ?? ""; - g_free(intPtr2); - g_object_unref(intPtr); - _fileDialogTcs?.TrySetResult(new FileDialogResult - { - Accepted = true, - SelectedFiles = new string[1] { text } - }); - } - else - { - _fileDialogTcs?.TrySetResult(new FileDialogResult - { - Accepted = false - }); - } - } - catch - { - _fileDialogTcs?.TrySetResult(new FileDialogResult - { - Accepted = false - }); - } - }; - if (action == GtkFileChooserAction.Open && !allowMultiple) - { - gtk_file_dialog_open(dialog, IntPtr.Zero, IntPtr.Zero, _currentCallback, IntPtr.Zero); - } - else if (action == GtkFileChooserAction.Open && allowMultiple) - { - gtk_file_dialog_open_multiple(dialog, IntPtr.Zero, IntPtr.Zero, _currentCallback, IntPtr.Zero); - } - else if (action == GtkFileChooserAction.Save) - { - gtk_file_dialog_save(dialog, IntPtr.Zero, IntPtr.Zero, _currentCallback, IntPtr.Zero); - } - else if (action == GtkFileChooserAction.SelectFolder) - { - gtk_file_dialog_select_folder(dialog, IntPtr.Zero, IntPtr.Zero, _currentCallback, IntPtr.Zero); - } - while (!_fileDialogTcs.Task.IsCompleted) - { - ProcessPendingEvents(); - Thread.Sleep(10); - } - g_object_unref(dialog); - return _fileDialogTcs.Task.Result; - } - - private FileDialogResult ShowGtk3FileDialog(string title, int action, bool allowMultiple, (string Name, string Pattern)[] filters) - { - IntPtr intPtr = gtk3_file_chooser_dialog_new(title, IntPtr.Zero, action, "_Cancel", -6, (action == 1) ? "_Save" : "_Open", -3, IntPtr.Zero); - if (allowMultiple) - { - gtk3_file_chooser_set_select_multiple(intPtr, selectMultiple: true); - } - int num = gtk3_dialog_run(intPtr); - FileDialogResult result = new FileDialogResult - { - Accepted = false - }; - if (num == -3) - { - if (allowMultiple) - { - IntPtr list = gtk3_file_chooser_get_filenames(intPtr); - uint num2 = g_slist_length(list); - List list2 = new List(); - for (uint num3 = 0u; num3 < num2; num3++) - { - IntPtr intPtr2 = g_slist_nth_data(list, num3); - string text = Marshal.PtrToStringUTF8(intPtr2); - if (!string.IsNullOrEmpty(text)) - { - list2.Add(text); - g_free(intPtr2); - } - } - g_slist_free(list); - result = new FileDialogResult - { - Accepted = true, - SelectedFiles = list2.ToArray() - }; - } - else - { - IntPtr intPtr3 = gtk3_file_chooser_get_filename(intPtr); - string text2 = Marshal.PtrToStringUTF8(intPtr3); - g_free(intPtr3); - if (!string.IsNullOrEmpty(text2)) - { - FileDialogResult fileDialogResult = new FileDialogResult(); - fileDialogResult.Accepted = true; - fileDialogResult.SelectedFiles = new string[1] { text2 }; - result = fileDialogResult; - } - } - } - gtk3_widget_destroy(intPtr); - ProcessPendingEvents(); - return result; - } - - public ColorDialogResult ShowColorDialog(string title = "Choose Color", float initialRed = 1f, float initialGreen = 1f, float initialBlue = 1f, float initialAlpha = 1f, bool withAlpha = true) - { - if (!EnsureInitialized()) - { - return new ColorDialogResult - { - Accepted = false - }; - } - if (_useGtk4) - { - return ShowGtk4ColorDialog(title, initialRed, initialGreen, initialBlue, initialAlpha, withAlpha); - } - return new ColorDialogResult - { - Accepted = false - }; - } - - private ColorDialogResult ShowGtk4ColorDialog(string title, float r, float g, float b, float a, bool withAlpha) - { - IntPtr dialog = gtk_color_dialog_new(); - gtk_color_dialog_set_title(dialog, title); - gtk_color_dialog_set_modal(dialog, modal: true); - gtk_color_dialog_set_with_alpha(dialog, withAlpha); - _colorDialogTcs = new TaskCompletionSource(); - _currentCallback = delegate(IntPtr source, IntPtr result, IntPtr userData) - { - IntPtr error = IntPtr.Zero; - try - { - IntPtr intPtr = gtk_color_dialog_choose_rgba_finish(dialog, result, out error); - if (intPtr != IntPtr.Zero && error == IntPtr.Zero) - { - GdkRGBA gdkRGBA = Marshal.PtrToStructure(intPtr); - _colorDialogTcs?.TrySetResult(new ColorDialogResult - { - Accepted = true, - Red = gdkRGBA.Red, - Green = gdkRGBA.Green, - Blue = gdkRGBA.Blue, - Alpha = gdkRGBA.Alpha - }); - } - else - { - _colorDialogTcs?.TrySetResult(new ColorDialogResult - { - Accepted = false - }); - } - } - catch - { - _colorDialogTcs?.TrySetResult(new ColorDialogResult - { - Accepted = false - }); - } - }; - gtk_color_dialog_choose_rgba(dialog, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, _currentCallback, IntPtr.Zero); - while (!_colorDialogTcs.Task.IsCompleted) - { - ProcessPendingEvents(); - Thread.Sleep(10); - } - g_object_unref(dialog); - return _colorDialogTcs.Task.Result; - } - - private bool EnsureInitialized() - { - if (!_initialized) - { - Initialize(); - } - return _initialized; - } - - private void ProcessPendingEvents() - { - IntPtr context = g_main_context_default(); - while (g_main_context_iteration(context, mayBlock: false)) - { - } - } - - public void Dispose() - { - if (!_disposed) - { - _disposed = true; - _initialized = false; - GC.SuppressFinalize(this); - } - } - - ~Gtk4InteropService() - { - Dispose(); - } + [DllImport(LibGtk3, EntryPoint = "gtk_file_chooser_dialog_new")] + private static extern IntPtr gtk3_file_chooser_dialog_new( + [MarshalAs(UnmanagedType.LPStr)] string title, + IntPtr parent, + int action, + [MarshalAs(UnmanagedType.LPStr)] string firstButtonText, + int firstButtonResponse, + [MarshalAs(UnmanagedType.LPStr)] string secondButtonText, + int secondButtonResponse, + IntPtr terminator); + + [DllImport(LibGtk3, EntryPoint = "gtk_dialog_run")] + private static extern int gtk3_dialog_run(IntPtr dialog); + + [DllImport(LibGtk3, EntryPoint = "gtk_widget_destroy")] + private static extern void gtk3_widget_destroy(IntPtr widget); + + [DllImport(LibGtk3, EntryPoint = "gtk_file_chooser_get_filename")] + private static extern IntPtr gtk3_file_chooser_get_filename(IntPtr chooser); + + [DllImport(LibGtk3, EntryPoint = "gtk_file_chooser_get_filenames")] + private static extern IntPtr gtk3_file_chooser_get_filenames(IntPtr chooser); + + [DllImport(LibGtk3, EntryPoint = "gtk_file_chooser_set_select_multiple")] + private static extern void gtk3_file_chooser_set_select_multiple(IntPtr chooser, bool selectMultiple); + + [DllImport(LibGtk3, EntryPoint = "gtk_message_dialog_new")] + private static extern IntPtr gtk3_message_dialog_new( + IntPtr parent, + int flags, + int type, + int buttons, + [MarshalAs(UnmanagedType.LPStr)] string message); + + [DllImport(LibGlib, EntryPoint = "g_slist_length")] + private static extern uint g_slist_length(IntPtr list); + + [DllImport(LibGlib, EntryPoint = "g_slist_nth_data")] + private static extern IntPtr g_slist_nth_data(IntPtr list, uint n); + + [DllImport(LibGlib, EntryPoint = "g_slist_free")] + private static extern void g_slist_free(IntPtr list); + + #endregion + + #region Fields + + private bool _initialized; + private bool _useGtk4; + private bool _disposed; + private readonly object _lock = new(); + + // Store callbacks to prevent GC + private GAsyncReadyCallback? _currentCallback; + private TaskCompletionSource? _fileDialogTcs; + private TaskCompletionSource? _colorDialogTcs; + private IntPtr _currentDialog; + + #endregion + + #region Properties + + /// + /// Gets whether GTK is initialized. + /// + public bool IsInitialized => _initialized; + + /// + /// Gets whether GTK4 is being used (vs GTK3 fallback). + /// + public bool IsGtk4 => _useGtk4; + + #endregion + + #region Initialization + + /// + /// Initializes the GTK4 interop service. + /// Falls back to GTK3 if GTK4 is not available. + /// + public bool Initialize() + { + if (_initialized) + return true; + + lock (_lock) + { + if (_initialized) + return true; + + // Try GTK4 first + try + { + if (gtk_init_check()) + { + _useGtk4 = true; + _initialized = true; + Console.WriteLine("[GTK4] Initialized GTK4"); + return true; + } + } + catch (DllNotFoundException) + { + Console.WriteLine("[GTK4] GTK4 not found, trying GTK3"); + } + catch (Exception ex) + { + Console.WriteLine($"[GTK4] GTK4 init failed: {ex.Message}"); + } + + // Fall back to GTK3 + try + { + int argc = 0; + IntPtr argv = IntPtr.Zero; + if (gtk3_init_check(ref argc, ref argv)) + { + _useGtk4 = false; + _initialized = true; + Console.WriteLine("[GTK4] Initialized GTK3 (fallback)"); + return true; + } + } + catch (DllNotFoundException) + { + Console.WriteLine("[GTK4] GTK3 not found"); + } + catch (Exception ex) + { + Console.WriteLine($"[GTK4] GTK3 init failed: {ex.Message}"); + } + + return false; + } + } + + #endregion + + #region Message Dialogs + + /// + /// Shows an alert message dialog. + /// + public void ShowAlert(string title, string message, GtkMessageType type = GtkMessageType.Info) + { + if (!EnsureInitialized()) + return; + + if (_useGtk4) + { + var dialog = gtk_alert_dialog_new(title); + gtk_alert_dialog_set_detail(dialog, message); + string[] buttons = { "OK" }; + gtk_alert_dialog_set_buttons(dialog, buttons); + gtk_alert_dialog_show(dialog, IntPtr.Zero); + g_object_unref(dialog); + } + else + { + var dialog = gtk3_message_dialog_new( + IntPtr.Zero, + 1, // GTK_DIALOG_MODAL + (int)type, + (int)GtkButtonsType.Ok, + message); + + gtk3_dialog_run(dialog); + gtk3_widget_destroy(dialog); + } + + ProcessPendingEvents(); + } + + /// + /// Shows a confirmation dialog. + /// + public bool ShowConfirmation(string title, string message) + { + if (!EnsureInitialized()) + return false; + + if (_useGtk4) + { + // GTK4 async dialogs are more complex - use synchronous approach + var dialog = gtk_alert_dialog_new(title); + gtk_alert_dialog_set_detail(dialog, message); + string[] buttons = { "No", "Yes" }; + gtk_alert_dialog_set_buttons(dialog, buttons); + gtk_alert_dialog_set_default_button(dialog, 1); + gtk_alert_dialog_set_cancel_button(dialog, 0); + gtk_alert_dialog_show(dialog, IntPtr.Zero); + g_object_unref(dialog); + // Note: GTK4 alert dialogs are async, this is simplified + return true; + } + else + { + var dialog = gtk3_message_dialog_new( + IntPtr.Zero, + 1, // GTK_DIALOG_MODAL + (int)GtkMessageType.Question, + (int)GtkButtonsType.YesNo, + message); + + int response = gtk3_dialog_run(dialog); + gtk3_widget_destroy(dialog); + ProcessPendingEvents(); + + return response == (int)GtkResponseType.Yes; + } + } + + #endregion + + #region File Dialogs + + /// + /// Shows an open file dialog. + /// + public FileDialogResult ShowOpenFileDialog( + string title = "Open File", + string? initialFolder = null, + bool allowMultiple = false, + params (string Name, string Pattern)[] filters) + { + if (!EnsureInitialized()) + return new FileDialogResult { Accepted = false }; + + if (_useGtk4) + { + return ShowGtk4FileDialog(title, GtkFileChooserAction.Open, allowMultiple, filters); + } + else + { + return ShowGtk3FileDialog(title, 0, allowMultiple, filters); // GTK_FILE_CHOOSER_ACTION_OPEN = 0 + } + } + + /// + /// Shows a save file dialog. + /// + public FileDialogResult ShowSaveFileDialog( + string title = "Save File", + string? suggestedName = null, + params (string Name, string Pattern)[] filters) + { + if (!EnsureInitialized()) + return new FileDialogResult { Accepted = false }; + + if (_useGtk4) + { + return ShowGtk4FileDialog(title, GtkFileChooserAction.Save, false, filters); + } + else + { + return ShowGtk3FileDialog(title, 1, false, filters); // GTK_FILE_CHOOSER_ACTION_SAVE = 1 + } + } + + /// + /// Shows a folder picker dialog. + /// + public FileDialogResult ShowFolderDialog(string title = "Select Folder") + { + if (!EnsureInitialized()) + return new FileDialogResult { Accepted = false }; + + if (_useGtk4) + { + return ShowGtk4FileDialog(title, GtkFileChooserAction.SelectFolder, false, Array.Empty<(string, string)>()); + } + else + { + return ShowGtk3FileDialog(title, 2, false, Array.Empty<(string, string)>()); // GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER = 2 + } + } + + private FileDialogResult ShowGtk4FileDialog( + string title, + GtkFileChooserAction action, + bool allowMultiple, + (string Name, string Pattern)[] filters) + { + var dialog = gtk_file_dialog_new(); + gtk_file_dialog_set_title(dialog, title); + gtk_file_dialog_set_modal(dialog, true); + + // Set up filters + if (filters.Length > 0) + { + var filter = gtk_file_filter_new(); + gtk_file_filter_set_name(filter, filters[0].Name); + gtk_file_filter_add_pattern(filter, filters[0].Pattern); + gtk_file_dialog_set_default_filter(dialog, filter); + } + + // For GTK4, we need async handling - simplified synchronous version + // In a full implementation, this would use proper async/await + _fileDialogTcs = new TaskCompletionSource(); + _currentDialog = dialog; + + _currentCallback = (source, result, userData) => + { + IntPtr error = IntPtr.Zero; + IntPtr file = IntPtr.Zero; + + try + { + if (action == GtkFileChooserAction.Open && !allowMultiple) + file = gtk_file_dialog_open_finish(dialog, result, out error); + else if (action == GtkFileChooserAction.Save) + file = gtk_file_dialog_save_finish(dialog, result, out error); + else if (action == GtkFileChooserAction.SelectFolder) + file = gtk_file_dialog_select_folder_finish(dialog, result, out error); + + if (file != IntPtr.Zero && error == IntPtr.Zero) + { + IntPtr pathPtr = g_file_get_path(file); + string path = Marshal.PtrToStringUTF8(pathPtr) ?? ""; + g_free(pathPtr); + g_object_unref(file); + + _fileDialogTcs?.TrySetResult(new FileDialogResult + { + Accepted = true, + SelectedFiles = new[] { path } + }); + } + else + { + _fileDialogTcs?.TrySetResult(new FileDialogResult { Accepted = false }); + } + } + catch + { + _fileDialogTcs?.TrySetResult(new FileDialogResult { Accepted = false }); + } + }; + + // Start the dialog + if (action == GtkFileChooserAction.Open && !allowMultiple) + gtk_file_dialog_open(dialog, IntPtr.Zero, IntPtr.Zero, _currentCallback, IntPtr.Zero); + else if (action == GtkFileChooserAction.Open && allowMultiple) + gtk_file_dialog_open_multiple(dialog, IntPtr.Zero, IntPtr.Zero, _currentCallback, IntPtr.Zero); + else if (action == GtkFileChooserAction.Save) + gtk_file_dialog_save(dialog, IntPtr.Zero, IntPtr.Zero, _currentCallback, IntPtr.Zero); + else if (action == GtkFileChooserAction.SelectFolder) + gtk_file_dialog_select_folder(dialog, IntPtr.Zero, IntPtr.Zero, _currentCallback, IntPtr.Zero); + + // Process events until dialog completes + while (!_fileDialogTcs.Task.IsCompleted) + { + ProcessPendingEvents(); + Thread.Sleep(10); + } + + g_object_unref(dialog); + return _fileDialogTcs.Task.Result; + } + + private FileDialogResult ShowGtk3FileDialog( + string title, + int action, + bool allowMultiple, + (string Name, string Pattern)[] filters) + { + var dialog = gtk3_file_chooser_dialog_new( + title, + IntPtr.Zero, + action, + "_Cancel", (int)GtkResponseType.Cancel, + action == 1 ? "_Save" : "_Open", (int)GtkResponseType.Accept, + IntPtr.Zero); + + if (allowMultiple) + gtk3_file_chooser_set_select_multiple(dialog, true); + + int response = gtk3_dialog_run(dialog); + + var result = new FileDialogResult { Accepted = false }; + + if (response == (int)GtkResponseType.Accept) + { + if (allowMultiple) + { + IntPtr list = gtk3_file_chooser_get_filenames(dialog); + uint count = g_slist_length(list); + var files = new List(); + + for (uint i = 0; i < count; i++) + { + IntPtr pathPtr = g_slist_nth_data(list, i); + string? path = Marshal.PtrToStringUTF8(pathPtr); + if (!string.IsNullOrEmpty(path)) + { + files.Add(path); + g_free(pathPtr); + } + } + + g_slist_free(list); + result = new FileDialogResult { Accepted = true, SelectedFiles = files.ToArray() }; + } + else + { + IntPtr pathPtr = gtk3_file_chooser_get_filename(dialog); + string? path = Marshal.PtrToStringUTF8(pathPtr); + g_free(pathPtr); + + if (!string.IsNullOrEmpty(path)) + result = new FileDialogResult { Accepted = true, SelectedFiles = new[] { path } }; + } + } + + gtk3_widget_destroy(dialog); + ProcessPendingEvents(); + + return result; + } + + #endregion + + #region Color Dialog + + /// + /// Shows a color picker dialog. + /// + public ColorDialogResult ShowColorDialog( + string title = "Choose Color", + float initialRed = 1f, + float initialGreen = 1f, + float initialBlue = 1f, + float initialAlpha = 1f, + bool withAlpha = true) + { + if (!EnsureInitialized()) + return new ColorDialogResult { Accepted = false }; + + if (_useGtk4) + { + return ShowGtk4ColorDialog(title, initialRed, initialGreen, initialBlue, initialAlpha, withAlpha); + } + else + { + // GTK3 color dialog would go here + return new ColorDialogResult { Accepted = false }; + } + } + + private ColorDialogResult ShowGtk4ColorDialog( + string title, + float r, float g, float b, float a, + bool withAlpha) + { + var dialog = gtk_color_dialog_new(); + gtk_color_dialog_set_title(dialog, title); + gtk_color_dialog_set_modal(dialog, true); + gtk_color_dialog_set_with_alpha(dialog, withAlpha); + + _colorDialogTcs = new TaskCompletionSource(); + + _currentCallback = (source, result, userData) => + { + IntPtr error = IntPtr.Zero; + try + { + IntPtr rgbaPtr = gtk_color_dialog_choose_rgba_finish(dialog, result, out error); + if (rgbaPtr != IntPtr.Zero && error == IntPtr.Zero) + { + var rgba = Marshal.PtrToStructure(rgbaPtr); + _colorDialogTcs?.TrySetResult(new ColorDialogResult + { + Accepted = true, + Red = rgba.Red, + Green = rgba.Green, + Blue = rgba.Blue, + Alpha = rgba.Alpha + }); + } + else + { + _colorDialogTcs?.TrySetResult(new ColorDialogResult { Accepted = false }); + } + } + catch + { + _colorDialogTcs?.TrySetResult(new ColorDialogResult { Accepted = false }); + } + }; + + gtk_color_dialog_choose_rgba(dialog, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, _currentCallback, IntPtr.Zero); + + while (!_colorDialogTcs.Task.IsCompleted) + { + ProcessPendingEvents(); + Thread.Sleep(10); + } + + g_object_unref(dialog); + return _colorDialogTcs.Task.Result; + } + + #endregion + + #region Helpers + + private bool EnsureInitialized() + { + if (!_initialized) + Initialize(); + return _initialized; + } + + private void ProcessPendingEvents() + { + var context = g_main_context_default(); + while (g_main_context_iteration(context, false)) { } + } + + #endregion + + #region IDisposable + + public void Dispose() + { + if (_disposed) + return; + + _disposed = true; + _initialized = false; + + GC.SuppressFinalize(this); + } + + ~Gtk4InteropService() + { + Dispose(); + } + + #endregion } diff --git a/Services/GtkButtonsType.cs b/Services/GtkButtonsType.cs deleted file mode 100644 index 6d17ffc..0000000 --- a/Services/GtkButtonsType.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Microsoft.Maui.Platform.Linux.Services; - -public enum GtkButtonsType -{ - None, - Ok, - Close, - Cancel, - YesNo, - OkCancel -} diff --git a/Services/GtkContextMenuService.cs b/Services/GtkContextMenuService.cs deleted file mode 100644 index e095769..0000000 --- a/Services/GtkContextMenuService.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Runtime.InteropServices; -using Microsoft.Maui.Platform.Linux.Native; - -namespace Microsoft.Maui.Platform.Linux.Services; - -public static class GtkContextMenuService -{ - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - private delegate void ActivateCallback(IntPtr menuItem, IntPtr userData); - - private static readonly List _callbacks = new List(); - - private static readonly List _actions = new List(); - - public static void ShowContextMenu(List items) - { - if (items == null || items.Count == 0) - { - return; - } - _callbacks.Clear(); - _actions.Clear(); - IntPtr intPtr = GtkNative.gtk_menu_new(); - if (intPtr == IntPtr.Zero) - { - Console.WriteLine("[GtkContextMenuService] Failed to create GTK menu"); - return; - } - foreach (GtkMenuItem item in items) - { - IntPtr intPtr2; - if (item.IsSeparator) - { - intPtr2 = GtkNative.gtk_separator_menu_item_new(); - } - else - { - intPtr2 = GtkNative.gtk_menu_item_new_with_label(item.Text); - GtkNative.gtk_widget_set_sensitive(intPtr2, item.IsEnabled); - if (item.IsEnabled && item.Action != null) - { - Action action = item.Action; - _actions.Add(action); - int actionIndex = _actions.Count - 1; - ActivateCallback activateCallback = delegate - { - Console.WriteLine("[GtkContextMenuService] Menu item activated: " + item.Text); - _actions[actionIndex]?.Invoke(); - }; - _callbacks.Add(activateCallback); - GtkNative.g_signal_connect_data(intPtr2, "activate", Marshal.GetFunctionPointerForDelegate(activateCallback), IntPtr.Zero, IntPtr.Zero, 0); - } - } - GtkNative.gtk_menu_shell_append(intPtr, intPtr2); - GtkNative.gtk_widget_show(intPtr2); - } - GtkNative.gtk_widget_show(intPtr); - IntPtr intPtr3 = GtkNative.gtk_get_current_event(); - GtkNative.gtk_menu_popup_at_pointer(intPtr, intPtr3); - if (intPtr3 != IntPtr.Zero) - { - GtkNative.gdk_event_free(intPtr3); - } - Console.WriteLine($"[GtkContextMenuService] Showed GTK menu with {items.Count} items"); - } -} diff --git a/Services/GtkFileChooserAction.cs b/Services/GtkFileChooserAction.cs deleted file mode 100644 index 10c60ca..0000000 --- a/Services/GtkFileChooserAction.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Microsoft.Maui.Platform.Linux.Services; - -public enum GtkFileChooserAction -{ - Open, - Save, - SelectFolder, - CreateFolder -} diff --git a/Services/GtkHostService.cs b/Services/GtkHostService.cs deleted file mode 100644 index b54e893..0000000 --- a/Services/GtkHostService.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System; -using Microsoft.Maui.Platform.Linux.Handlers; -using Microsoft.Maui.Platform.Linux.Window; - -namespace Microsoft.Maui.Platform.Linux.Services; - -public class GtkHostService -{ - private static GtkHostService? _instance; - - private GtkHostWindow? _hostWindow; - - private GtkWebViewManager? _webViewManager; - - public static GtkHostService Instance => _instance ?? (_instance = new GtkHostService()); - - public GtkHostWindow? HostWindow => _hostWindow; - - public GtkWebViewManager? WebViewManager => _webViewManager; - - public bool IsInitialized => _hostWindow != null; - - public event EventHandler? HostWindowCreated; - - public void Initialize(string title, int width, int height) - { - if (_hostWindow == null) - { - _hostWindow = new GtkHostWindow(title, width, height); - _webViewManager = new GtkWebViewManager(_hostWindow); - this.HostWindowCreated?.Invoke(this, _hostWindow); - } - } - - public GtkHostWindow GetOrCreateHostWindow(string title = "MAUI Application", int width = 800, int height = 600) - { - if (_hostWindow == null) - { - Initialize(title, width, height); - } - return _hostWindow; - } - - public void SetWindowIcon(string iconPath) - { - _hostWindow?.SetIcon(iconPath); - } - - public void Shutdown() - { - _webViewManager?.Clear(); - _webViewManager = null; - _hostWindow?.Dispose(); - _hostWindow = null; - } -} diff --git a/Services/GtkMenuItem.cs b/Services/GtkMenuItem.cs deleted file mode 100644 index 21282c1..0000000 --- a/Services/GtkMenuItem.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; - -namespace Microsoft.Maui.Platform.Linux.Services; - -public class GtkMenuItem -{ - public string Text { get; } - - public Action? Action { get; } - - public bool IsEnabled { get; } - - public bool IsSeparator { get; } - - public static GtkMenuItem Separator => new GtkMenuItem(); - - public GtkMenuItem(string text, Action? action, bool isEnabled = true) - { - Text = text; - Action = action; - IsEnabled = isEnabled; - IsSeparator = false; - } - - private GtkMenuItem() - { - Text = ""; - Action = null; - IsEnabled = false; - IsSeparator = true; - } -} diff --git a/Services/GtkMessageType.cs b/Services/GtkMessageType.cs deleted file mode 100644 index 16b1643..0000000 --- a/Services/GtkMessageType.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Microsoft.Maui.Platform.Linux.Services; - -public enum GtkMessageType -{ - Info, - Warning, - Question, - Error, - Other -} diff --git a/Services/GtkResponseType.cs b/Services/GtkResponseType.cs deleted file mode 100644 index 0bce982..0000000 --- a/Services/GtkResponseType.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace Microsoft.Maui.Platform.Linux.Services; - -public enum GtkResponseType -{ - None = -1, - Reject = -2, - Accept = -3, - DeleteEvent = -4, - Ok = -5, - Cancel = -6, - Close = -7, - Yes = -8, - No = -9, - Apply = -10, - Help = -11 -} diff --git a/Services/HardwareVideoService.cs b/Services/HardwareVideoService.cs index 76c0aab..dc8d343 100644 --- a/Services/HardwareVideoService.cs +++ b/Services/HardwareVideoService.cs @@ -1,562 +1,722 @@ -using System; -using System.Collections.Generic; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using System.Runtime.InteropServices; using SkiaSharp; namespace Microsoft.Maui.Platform.Linux.Services; +/// +/// Supported hardware video acceleration APIs. +/// +public enum VideoAccelerationApi +{ + /// + /// Automatically select the best available API. + /// + Auto, + + /// + /// VA-API (Video Acceleration API) - Intel, AMD, and some NVIDIA. + /// + VaApi, + + /// + /// VDPAU (Video Decode and Presentation API for Unix) - NVIDIA. + /// + Vdpau, + + /// + /// Software decoding fallback. + /// + Software +} + +/// +/// Video codec profiles supported by hardware acceleration. +/// +public enum VideoProfile +{ + H264Baseline, + H264Main, + H264High, + H265Main, + H265Main10, + Vp8, + Vp9Profile0, + Vp9Profile2, + Av1Main +} + +/// +/// Information about a decoded video frame. +/// +public class VideoFrame : IDisposable +{ + public int Width { get; init; } + public int Height { get; init; } + public IntPtr DataY { get; init; } + public IntPtr DataU { get; init; } + public IntPtr DataV { get; init; } + public int StrideY { get; init; } + public int StrideU { get; init; } + public int StrideV { get; init; } + public long Timestamp { get; init; } + public bool IsKeyFrame { get; init; } + + private bool _disposed; + private Action? _releaseCallback; + + internal void SetReleaseCallback(Action callback) => _releaseCallback = callback; + + public void Dispose() + { + if (!_disposed) + { + _releaseCallback?.Invoke(); + _disposed = true; + } + } +} + +/// +/// Hardware-accelerated video decoding service using VA-API or VDPAU. +/// Provides efficient video decode for media playback on Linux. +/// public class HardwareVideoService : IDisposable { - private struct VaImage - { - public uint ImageId; + #region VA-API Native Interop - public uint Format; + private const string LibVa = "libva.so.2"; + private const string LibVaDrm = "libva-drm.so.2"; + private const string LibVaX11 = "libva-x11.so.2"; - public uint FormatFourCC; + // VA-API error codes + private const int VA_STATUS_SUCCESS = 0; - public int Width; + // VA-API profile constants + private const int VAProfileH264Baseline = 5; + private const int VAProfileH264Main = 6; + private const int VAProfileH264High = 7; + private const int VAProfileHEVCMain = 12; + private const int VAProfileHEVCMain10 = 13; + private const int VAProfileVP8Version0_3 = 14; + private const int VAProfileVP9Profile0 = 15; + private const int VAProfileVP9Profile2 = 17; + private const int VAProfileAV1Profile0 = 20; - public int Height; + // VA-API entrypoint + private const int VAEntrypointVLD = 1; // Video Decode - public uint DataSize; + // Surface formats + private const uint VA_RT_FORMAT_YUV420 = 0x00000001; + private const uint VA_RT_FORMAT_YUV420_10 = 0x00000100; - public uint NumPlanes; + [DllImport(LibVa)] + private static extern IntPtr vaGetDisplayDRM(int fd); - public uint PitchesPlane0; + [DllImport(LibVaX11)] + private static extern IntPtr vaGetDisplay(IntPtr x11Display); - public uint PitchesPlane1; + [DllImport(LibVa)] + private static extern int vaInitialize(IntPtr display, out int majorVersion, out int minorVersion); - public uint PitchesPlane2; + [DllImport(LibVa)] + private static extern int vaTerminate(IntPtr display); - public uint PitchesPlane3; + [DllImport(LibVa)] + private static extern IntPtr vaErrorStr(int errorCode); - public uint OffsetsPlane0; + [DllImport(LibVa)] + private static extern int vaQueryConfigProfiles(IntPtr display, [Out] int[] profileList, out int numProfiles); - public uint OffsetsPlane1; + [DllImport(LibVa)] + private static extern int vaQueryConfigEntrypoints(IntPtr display, int profile, [Out] int[] entrypoints, out int numEntrypoints); - public uint OffsetsPlane2; + [DllImport(LibVa)] + private static extern int vaCreateConfig(IntPtr display, int profile, int entrypoint, IntPtr attribList, int numAttribs, out uint configId); - public uint OffsetsPlane3; + [DllImport(LibVa)] + private static extern int vaDestroyConfig(IntPtr display, uint configId); - public uint BufferId; - } + [DllImport(LibVa)] + private static extern int vaCreateContext(IntPtr display, uint configId, int pictureWidth, int pictureHeight, int flag, IntPtr renderTargets, int numRenderTargets, out uint contextId); - private const string LibVa = "libva.so.2"; + [DllImport(LibVa)] + private static extern int vaDestroyContext(IntPtr display, uint contextId); - private const string LibVaDrm = "libva-drm.so.2"; + [DllImport(LibVa)] + private static extern int vaCreateSurfaces(IntPtr display, uint format, uint width, uint height, [Out] uint[] surfaces, uint numSurfaces, IntPtr attribList, uint numAttribs); - private const string LibVaX11 = "libva-x11.so.2"; + [DllImport(LibVa)] + private static extern int vaDestroySurfaces(IntPtr display, [In] uint[] surfaces, int numSurfaces); - private const int VA_STATUS_SUCCESS = 0; + [DllImport(LibVa)] + private static extern int vaSyncSurface(IntPtr display, uint surfaceId); - private const int VAProfileH264Baseline = 5; + [DllImport(LibVa)] + private static extern int vaMapBuffer(IntPtr display, uint bufferId, out IntPtr data); - private const int VAProfileH264Main = 6; + [DllImport(LibVa)] + private static extern int vaUnmapBuffer(IntPtr display, uint bufferId); - private const int VAProfileH264High = 7; + [DllImport(LibVa)] + private static extern int vaDeriveImage(IntPtr display, uint surfaceId, out VaImage image); - private const int VAProfileHEVCMain = 12; + [DllImport(LibVa)] + private static extern int vaDestroyImage(IntPtr display, uint imageId); - private const int VAProfileHEVCMain10 = 13; + [StructLayout(LayoutKind.Sequential)] + private struct VaImage + { + public uint ImageId; + public uint Format; // VAImageFormat (simplified) + public uint FormatFourCC; + public int Width; + public int Height; + public uint DataSize; + public uint NumPlanes; + public uint PitchesPlane0; + public uint PitchesPlane1; + public uint PitchesPlane2; + public uint PitchesPlane3; + public uint OffsetsPlane0; + public uint OffsetsPlane1; + public uint OffsetsPlane2; + public uint OffsetsPlane3; + public uint BufferId; + } - private const int VAProfileVP8Version0_3 = 14; - - private const int VAProfileVP9Profile0 = 15; - - private const int VAProfileVP9Profile2 = 17; - - private const int VAProfileAV1Profile0 = 20; - - private const int VAEntrypointVLD = 1; - - private const uint VA_RT_FORMAT_YUV420 = 1u; - - private const uint VA_RT_FORMAT_YUV420_10 = 256u; - - private const string LibVdpau = "libvdpau.so.1"; - - private const int O_RDWR = 2; - - private IntPtr _vaDisplay; - - private uint _vaConfigId; - - private uint _vaContextId; - - private uint[] _vaSurfaces = Array.Empty(); - - private int _drmFd = -1; - - private bool _initialized; - - private bool _disposed; - - private VideoAccelerationApi _currentApi = VideoAccelerationApi.Software; - - private int _width; - - private int _height; - - private VideoProfile _profile; - - private readonly HashSet _supportedProfiles = new HashSet(); - - private readonly object _lock = new object(); - - public VideoAccelerationApi CurrentApi => _currentApi; - - public bool IsHardwareAccelerated - { - get - { - if (_currentApi != VideoAccelerationApi.Software) - { - return _initialized; - } - return false; - } - } - - public IReadOnlySet SupportedProfiles => _supportedProfiles; - - [DllImport("libva.so.2")] - private static extern IntPtr vaGetDisplayDRM(int fd); - - [DllImport("libva-x11.so.2")] - private static extern IntPtr vaGetDisplay(IntPtr x11Display); - - [DllImport("libva.so.2")] - private static extern int vaInitialize(IntPtr display, out int majorVersion, out int minorVersion); - - [DllImport("libva.so.2")] - private static extern int vaTerminate(IntPtr display); - - [DllImport("libva.so.2")] - private static extern IntPtr vaErrorStr(int errorCode); - - [DllImport("libva.so.2")] - private static extern int vaQueryConfigProfiles(IntPtr display, [Out] int[] profileList, out int numProfiles); - - [DllImport("libva.so.2")] - private static extern int vaQueryConfigEntrypoints(IntPtr display, int profile, [Out] int[] entrypoints, out int numEntrypoints); - - [DllImport("libva.so.2")] - private static extern int vaCreateConfig(IntPtr display, int profile, int entrypoint, IntPtr attribList, int numAttribs, out uint configId); - - [DllImport("libva.so.2")] - private static extern int vaDestroyConfig(IntPtr display, uint configId); - - [DllImport("libva.so.2")] - private static extern int vaCreateContext(IntPtr display, uint configId, int pictureWidth, int pictureHeight, int flag, IntPtr renderTargets, int numRenderTargets, out uint contextId); - - [DllImport("libva.so.2")] - private static extern int vaDestroyContext(IntPtr display, uint contextId); - - [DllImport("libva.so.2")] - private static extern int vaCreateSurfaces(IntPtr display, uint format, uint width, uint height, [Out] uint[] surfaces, uint numSurfaces, IntPtr attribList, uint numAttribs); - - [DllImport("libva.so.2")] - private static extern int vaDestroySurfaces(IntPtr display, [In] uint[] surfaces, int numSurfaces); - - [DllImport("libva.so.2")] - private static extern int vaSyncSurface(IntPtr display, uint surfaceId); - - [DllImport("libva.so.2")] - private static extern int vaMapBuffer(IntPtr display, uint bufferId, out IntPtr data); - - [DllImport("libva.so.2")] - private static extern int vaUnmapBuffer(IntPtr display, uint bufferId); - - [DllImport("libva.so.2")] - private static extern int vaDeriveImage(IntPtr display, uint surfaceId, out VaImage image); - - [DllImport("libva.so.2")] - private static extern int vaDestroyImage(IntPtr display, uint imageId); - - [DllImport("libvdpau.so.1")] - private static extern int vdp_device_create_x11(IntPtr display, int screen, out IntPtr device, out IntPtr getProcAddress); - - [DllImport("libc")] - private static extern int open([MarshalAs(UnmanagedType.LPStr)] string path, int flags); - - [DllImport("libc")] - private static extern int close(int fd); - - public bool Initialize(VideoAccelerationApi api = VideoAccelerationApi.Auto, IntPtr x11Display = 0) - { - if (_initialized) - { - return true; - } - lock (_lock) - { - if (_initialized) - { - return true; - } - if ((api == VideoAccelerationApi.Auto || api == VideoAccelerationApi.VaApi) && TryInitializeVaApi(x11Display)) - { - _currentApi = VideoAccelerationApi.VaApi; - _initialized = true; - Console.WriteLine($"[HardwareVideo] Initialized VA-API with {_supportedProfiles.Count} supported profiles"); - return true; - } - if ((api == VideoAccelerationApi.Auto || api == VideoAccelerationApi.Vdpau) && TryInitializeVdpau(x11Display)) - { - _currentApi = VideoAccelerationApi.Vdpau; - _initialized = true; - Console.WriteLine("[HardwareVideo] Initialized VDPAU"); - return true; - } - Console.WriteLine("[HardwareVideo] No hardware acceleration available, using software"); - _currentApi = VideoAccelerationApi.Software; - return false; - } - } - - private bool TryInitializeVaApi(IntPtr x11Display) - { - try - { - string[] array = new string[3] { "/dev/dri/renderD128", "/dev/dri/renderD129", "/dev/dri/card0" }; - foreach (string path in array) - { - _drmFd = open(path, 2); - if (_drmFd >= 0) - { - _vaDisplay = vaGetDisplayDRM(_drmFd); - if (_vaDisplay != IntPtr.Zero && InitializeVaDisplay()) - { - return true; - } - close(_drmFd); - _drmFd = -1; - } - } - if (x11Display != IntPtr.Zero) - { - _vaDisplay = vaGetDisplay(x11Display); - if (_vaDisplay != IntPtr.Zero && InitializeVaDisplay()) - { - return true; - } - } - return false; - } - catch (DllNotFoundException) - { - Console.WriteLine("[HardwareVideo] VA-API libraries not found"); - return false; - } - catch (Exception ex2) - { - Console.WriteLine("[HardwareVideo] VA-API initialization failed: " + ex2.Message); - return false; - } - } - - private bool InitializeVaDisplay() - { - int majorVersion; - int minorVersion; - int num = vaInitialize(_vaDisplay, out majorVersion, out minorVersion); - if (num != 0) - { - Console.WriteLine("[HardwareVideo] vaInitialize failed: " + GetVaError(num)); - return false; - } - Console.WriteLine($"[HardwareVideo] VA-API {majorVersion}.{minorVersion} initialized"); - int[] array = new int[32]; - if (vaQueryConfigProfiles(_vaDisplay, array, out var numProfiles) == 0) - { - for (int i = 0; i < numProfiles; i++) - { - if (!TryMapVaProfile(array[i], out var profile)) - { - continue; - } - int[] array2 = new int[8]; - if (vaQueryConfigEntrypoints(_vaDisplay, array[i], array2, out var numEntrypoints) != 0) - { - continue; - } - for (int j = 0; j < numEntrypoints; j++) - { - if (array2[j] == 1) - { - _supportedProfiles.Add(profile); - break; - } - } - } - } - return true; - } - - private bool TryInitializeVdpau(IntPtr x11Display) - { - if (x11Display == IntPtr.Zero) - { - return false; - } - try - { - if (vdp_device_create_x11(x11Display, 0, out var device, out var _) == 0 && device != IntPtr.Zero) - { - _supportedProfiles.Add(VideoProfile.H264Baseline); - _supportedProfiles.Add(VideoProfile.H264Main); - _supportedProfiles.Add(VideoProfile.H264High); - return true; - } - } - catch (DllNotFoundException) - { - Console.WriteLine("[HardwareVideo] VDPAU libraries not found"); - } - catch (Exception ex2) - { - Console.WriteLine("[HardwareVideo] VDPAU initialization failed: " + ex2.Message); - } - return false; - } - - public bool CreateDecoder(VideoProfile profile, int width, int height) - { - if (!_initialized || _currentApi == VideoAccelerationApi.Software) - { - return false; - } - if (!_supportedProfiles.Contains(profile)) - { - Console.WriteLine($"[HardwareVideo] Profile {profile} not supported"); - return false; - } - lock (_lock) - { - DestroyDecoder(); - _width = width; - _height = height; - _profile = profile; - if (_currentApi == VideoAccelerationApi.VaApi) - { - return CreateVaApiDecoder(profile, width, height); - } - return false; - } - } - - private bool CreateVaApiDecoder(VideoProfile profile, int width, int height) - { - int profile2 = MapToVaProfile(profile); - int num = vaCreateConfig(_vaDisplay, profile2, 1, IntPtr.Zero, 0, out _vaConfigId); - if (num != 0) - { - Console.WriteLine("[HardwareVideo] vaCreateConfig failed: " + GetVaError(num)); - return false; - } - uint format = ((profile != VideoProfile.H265Main10 && profile != VideoProfile.Vp9Profile2) ? 1u : 256u); - _vaSurfaces = new uint[8]; - num = vaCreateSurfaces(_vaDisplay, format, (uint)width, (uint)height, _vaSurfaces, 8u, IntPtr.Zero, 0u); - if (num != 0) - { - Console.WriteLine("[HardwareVideo] vaCreateSurfaces failed: " + GetVaError(num)); - vaDestroyConfig(_vaDisplay, _vaConfigId); - return false; - } - num = vaCreateContext(_vaDisplay, _vaConfigId, width, height, 0, IntPtr.Zero, 0, out _vaContextId); - if (num != 0) - { - Console.WriteLine("[HardwareVideo] vaCreateContext failed: " + GetVaError(num)); - vaDestroySurfaces(_vaDisplay, _vaSurfaces, _vaSurfaces.Length); - vaDestroyConfig(_vaDisplay, _vaConfigId); - return false; - } - Console.WriteLine($"[HardwareVideo] Created decoder: {profile} {width}x{height}"); - return true; - } - - public void DestroyDecoder() - { - lock (_lock) - { - if (_currentApi == VideoAccelerationApi.VaApi && _vaDisplay != IntPtr.Zero) - { - if (_vaContextId != 0) - { - vaDestroyContext(_vaDisplay, _vaContextId); - _vaContextId = 0u; - } - if (_vaSurfaces.Length != 0) - { - vaDestroySurfaces(_vaDisplay, _vaSurfaces, _vaSurfaces.Length); - _vaSurfaces = Array.Empty(); - } - if (_vaConfigId != 0) - { - vaDestroyConfig(_vaDisplay, _vaConfigId); - _vaConfigId = 0u; - } - } - } - } - - public VideoFrame? GetDecodedFrame(int surfaceIndex, long timestamp, bool isKeyFrame) - { - if (!_initialized || _currentApi != VideoAccelerationApi.VaApi) - { - return null; - } - if (surfaceIndex < 0 || surfaceIndex >= _vaSurfaces.Length) - { - return null; - } - uint surfaceId = _vaSurfaces[surfaceIndex]; - if (vaSyncSurface(_vaDisplay, surfaceId) != 0) - { - return null; - } - if (vaDeriveImage(_vaDisplay, surfaceId, out var image) != 0) - { - return null; - } - if (vaMapBuffer(_vaDisplay, image.BufferId, out nint data) != 0) - { - vaDestroyImage(_vaDisplay, image.ImageId); - return null; - } - VideoFrame obj = new VideoFrame - { - Width = image.Width, - Height = image.Height, - DataY = data + (int)image.OffsetsPlane0, - DataU = data + (int)image.OffsetsPlane1, - DataV = data + (int)image.OffsetsPlane2, - StrideY = (int)image.PitchesPlane0, - StrideU = (int)image.PitchesPlane1, - StrideV = (int)image.PitchesPlane2, - Timestamp = timestamp, - IsKeyFrame = isKeyFrame - }; - obj.SetReleaseCallback(delegate - { - vaUnmapBuffer(_vaDisplay, image.BufferId); - vaDestroyImage(_vaDisplay, image.ImageId); - }); - return obj; - } - - public unsafe SKBitmap? ConvertFrameToSkia(VideoFrame frame) - { - //IL_0013: Unknown result type (might be due to invalid IL or missing references) - //IL_0019: Expected O, but got Unknown - if (frame == null) - { - return null; - } - SKBitmap val = new SKBitmap(frame.Width, frame.Height, (SKColorType)6, (SKAlphaType)1); - byte* dataY = (byte*)frame.DataY; - byte* dataU = (byte*)frame.DataU; - byte* dataV = (byte*)frame.DataV; - byte* pixels = (byte*)val.GetPixels(); - for (int i = 0; i < frame.Height; i++) - { - for (int j = 0; j < frame.Width; j++) - { - int num = i * frame.StrideY + j; - int num2 = i / 2 * frame.StrideU + j / 2; - byte num3 = dataY[num]; - int num4 = dataU[num2] - 128; - int num5 = dataV[num2] - 128; - int value = (int)((double)(int)num3 + 1.402 * (double)num5); - int value2 = (int)((double)(int)num3 - 0.344 * (double)num4 - 0.714 * (double)num5); - int value3 = (int)((double)(int)num3 + 1.772 * (double)num4); - value = Math.Clamp(value, 0, 255); - value2 = Math.Clamp(value2, 0, 255); - value3 = Math.Clamp(value3, 0, 255); - int num6 = (i * frame.Width + j) * 4; - pixels[num6] = (byte)value3; - pixels[num6 + 1] = (byte)value2; - pixels[num6 + 2] = (byte)value; - pixels[num6 + 3] = byte.MaxValue; - } - } - return val; - } - - private static bool TryMapVaProfile(int vaProfile, out VideoProfile profile) - { - profile = vaProfile switch - { - 5 => VideoProfile.H264Baseline, - 6 => VideoProfile.H264Main, - 7 => VideoProfile.H264High, - 12 => VideoProfile.H265Main, - 13 => VideoProfile.H265Main10, - 14 => VideoProfile.Vp8, - 15 => VideoProfile.Vp9Profile0, - 17 => VideoProfile.Vp9Profile2, - 20 => VideoProfile.Av1Main, - _ => VideoProfile.H264Main, - }; - if (vaProfile >= 5) - { - return vaProfile <= 20; - } - return false; - } - - private static int MapToVaProfile(VideoProfile profile) - { - return profile switch - { - VideoProfile.H264Baseline => 5, - VideoProfile.H264Main => 6, - VideoProfile.H264High => 7, - VideoProfile.H265Main => 12, - VideoProfile.H265Main10 => 13, - VideoProfile.Vp8 => 14, - VideoProfile.Vp9Profile0 => 15, - VideoProfile.Vp9Profile2 => 17, - VideoProfile.Av1Main => 20, - _ => 6, - }; - } - - private static string GetVaError(int status) - { - try - { - return Marshal.PtrToStringAnsi(vaErrorStr(status)) ?? $"Unknown error {status}"; - } - catch - { - return $"Error code {status}"; - } - } - - public void Dispose() - { - if (!_disposed) - { - _disposed = true; - DestroyDecoder(); - if (_currentApi == VideoAccelerationApi.VaApi && _vaDisplay != IntPtr.Zero) - { - vaTerminate(_vaDisplay); - _vaDisplay = IntPtr.Zero; - } - if (_drmFd >= 0) - { - close(_drmFd); - _drmFd = -1; - } - GC.SuppressFinalize(this); - } - } - - ~HardwareVideoService() - { - Dispose(); - } + #endregion + + #region VDPAU Native Interop + + private const string LibVdpau = "libvdpau.so.1"; + + [DllImport(LibVdpau)] + private static extern int vdp_device_create_x11(IntPtr display, int screen, out IntPtr device, out IntPtr getProcAddress); + + #endregion + + #region DRM Interop + + [DllImport("libc", EntryPoint = "open")] + private static extern int open([MarshalAs(UnmanagedType.LPStr)] string path, int flags); + + [DllImport("libc", EntryPoint = "close")] + private static extern int close(int fd); + + private const int O_RDWR = 2; + + #endregion + + #region Fields + + private IntPtr _vaDisplay; + private uint _vaConfigId; + private uint _vaContextId; + private uint[] _vaSurfaces = Array.Empty(); + private int _drmFd = -1; + private bool _initialized; + private bool _disposed; + + private VideoAccelerationApi _currentApi = VideoAccelerationApi.Software; + private int _width; + private int _height; + private VideoProfile _profile; + + private readonly HashSet _supportedProfiles = new(); + private readonly object _lock = new(); + + #endregion + + #region Properties + + /// + /// Gets the currently active video acceleration API. + /// + public VideoAccelerationApi CurrentApi => _currentApi; + + /// + /// Gets whether hardware acceleration is available and initialized. + /// + public bool IsHardwareAccelerated => _currentApi != VideoAccelerationApi.Software && _initialized; + + /// + /// Gets the supported video profiles. + /// + public IReadOnlySet SupportedProfiles => _supportedProfiles; + + #endregion + + #region Initialization + + /// + /// Creates a new hardware video service. + /// + public HardwareVideoService() + { + } + + /// + /// Initializes the hardware video acceleration. + /// + /// The preferred API to use. + /// Optional X11 display for VA-API X11 backend. + /// True if initialization succeeded. + public bool Initialize(VideoAccelerationApi api = VideoAccelerationApi.Auto, IntPtr x11Display = default) + { + if (_initialized) + return true; + + lock (_lock) + { + if (_initialized) + return true; + + // Try VA-API first (works with Intel, AMD, and some NVIDIA) + if (api == VideoAccelerationApi.Auto || api == VideoAccelerationApi.VaApi) + { + if (TryInitializeVaApi(x11Display)) + { + _currentApi = VideoAccelerationApi.VaApi; + _initialized = true; + Console.WriteLine($"[HardwareVideo] Initialized VA-API with {_supportedProfiles.Count} supported profiles"); + return true; + } + } + + // Try VDPAU (NVIDIA proprietary) + if (api == VideoAccelerationApi.Auto || api == VideoAccelerationApi.Vdpau) + { + if (TryInitializeVdpau(x11Display)) + { + _currentApi = VideoAccelerationApi.Vdpau; + _initialized = true; + Console.WriteLine("[HardwareVideo] Initialized VDPAU"); + return true; + } + } + + Console.WriteLine("[HardwareVideo] No hardware acceleration available, using software"); + _currentApi = VideoAccelerationApi.Software; + return false; + } + } + + private bool TryInitializeVaApi(IntPtr x11Display) + { + try + { + // Try DRM backend first (works in Wayland and headless) + string[] drmDevices = { "/dev/dri/renderD128", "/dev/dri/renderD129", "/dev/dri/card0" }; + foreach (var device in drmDevices) + { + _drmFd = open(device, O_RDWR); + if (_drmFd >= 0) + { + _vaDisplay = vaGetDisplayDRM(_drmFd); + if (_vaDisplay != IntPtr.Zero) + { + if (InitializeVaDisplay()) + return true; + } + close(_drmFd); + _drmFd = -1; + } + } + + // Fall back to X11 backend if display provided + if (x11Display != IntPtr.Zero) + { + _vaDisplay = vaGetDisplay(x11Display); + if (_vaDisplay != IntPtr.Zero && InitializeVaDisplay()) + return true; + } + + return false; + } + catch (DllNotFoundException) + { + Console.WriteLine("[HardwareVideo] VA-API libraries not found"); + return false; + } + catch (Exception ex) + { + Console.WriteLine($"[HardwareVideo] VA-API initialization failed: {ex.Message}"); + return false; + } + } + + private bool InitializeVaDisplay() + { + int status = vaInitialize(_vaDisplay, out int major, out int minor); + if (status != VA_STATUS_SUCCESS) + { + Console.WriteLine($"[HardwareVideo] vaInitialize failed: {GetVaError(status)}"); + return false; + } + + Console.WriteLine($"[HardwareVideo] VA-API {major}.{minor} initialized"); + + // Query supported profiles + int[] profiles = new int[32]; + status = vaQueryConfigProfiles(_vaDisplay, profiles, out int numProfiles); + if (status == VA_STATUS_SUCCESS) + { + for (int i = 0; i < numProfiles; i++) + { + if (TryMapVaProfile(profiles[i], out var videoProfile)) + { + // Check if VLD (decode) entrypoint is supported + int[] entrypoints = new int[8]; + if (vaQueryConfigEntrypoints(_vaDisplay, profiles[i], entrypoints, out int numEntrypoints) == VA_STATUS_SUCCESS) + { + for (int j = 0; j < numEntrypoints; j++) + { + if (entrypoints[j] == VAEntrypointVLD) + { + _supportedProfiles.Add(videoProfile); + break; + } + } + } + } + } + } + + return true; + } + + private bool TryInitializeVdpau(IntPtr x11Display) + { + if (x11Display == IntPtr.Zero) + return false; + + try + { + int result = vdp_device_create_x11(x11Display, 0, out IntPtr device, out IntPtr getProcAddress); + if (result == 0 && device != IntPtr.Zero) + { + // VDPAU initialized - would need additional setup for actual use + // For now, just mark as available + _supportedProfiles.Add(VideoProfile.H264Baseline); + _supportedProfiles.Add(VideoProfile.H264Main); + _supportedProfiles.Add(VideoProfile.H264High); + return true; + } + } + catch (DllNotFoundException) + { + Console.WriteLine("[HardwareVideo] VDPAU libraries not found"); + } + catch (Exception ex) + { + Console.WriteLine($"[HardwareVideo] VDPAU initialization failed: {ex.Message}"); + } + + return false; + } + + #endregion + + #region Decoder Creation + + /// + /// Creates a decoder context for the specified profile and dimensions. + /// + public bool CreateDecoder(VideoProfile profile, int width, int height) + { + if (!_initialized || _currentApi == VideoAccelerationApi.Software) + return false; + + if (!_supportedProfiles.Contains(profile)) + { + Console.WriteLine($"[HardwareVideo] Profile {profile} not supported"); + return false; + } + + lock (_lock) + { + // Destroy existing context + DestroyDecoder(); + + _width = width; + _height = height; + _profile = profile; + + if (_currentApi == VideoAccelerationApi.VaApi) + return CreateVaApiDecoder(profile, width, height); + + return false; + } + } + + private bool CreateVaApiDecoder(VideoProfile profile, int width, int height) + { + int vaProfile = MapToVaProfile(profile); + + // Create config + int status = vaCreateConfig(_vaDisplay, vaProfile, VAEntrypointVLD, IntPtr.Zero, 0, out _vaConfigId); + if (status != VA_STATUS_SUCCESS) + { + Console.WriteLine($"[HardwareVideo] vaCreateConfig failed: {GetVaError(status)}"); + return false; + } + + // Create surfaces for decoded frames (use a pool of 8) + uint format = profile == VideoProfile.H265Main10 || profile == VideoProfile.Vp9Profile2 + ? VA_RT_FORMAT_YUV420_10 + : VA_RT_FORMAT_YUV420; + + _vaSurfaces = new uint[8]; + status = vaCreateSurfaces(_vaDisplay, format, (uint)width, (uint)height, _vaSurfaces, 8, IntPtr.Zero, 0); + if (status != VA_STATUS_SUCCESS) + { + Console.WriteLine($"[HardwareVideo] vaCreateSurfaces failed: {GetVaError(status)}"); + vaDestroyConfig(_vaDisplay, _vaConfigId); + return false; + } + + // Create context + status = vaCreateContext(_vaDisplay, _vaConfigId, width, height, 0, IntPtr.Zero, 0, out _vaContextId); + if (status != VA_STATUS_SUCCESS) + { + Console.WriteLine($"[HardwareVideo] vaCreateContext failed: {GetVaError(status)}"); + vaDestroySurfaces(_vaDisplay, _vaSurfaces, _vaSurfaces.Length); + vaDestroyConfig(_vaDisplay, _vaConfigId); + return false; + } + + Console.WriteLine($"[HardwareVideo] Created decoder: {profile} {width}x{height}"); + return true; + } + + /// + /// Destroys the current decoder context. + /// + public void DestroyDecoder() + { + lock (_lock) + { + if (_currentApi == VideoAccelerationApi.VaApi && _vaDisplay != IntPtr.Zero) + { + if (_vaContextId != 0) + { + vaDestroyContext(_vaDisplay, _vaContextId); + _vaContextId = 0; + } + + if (_vaSurfaces.Length > 0) + { + vaDestroySurfaces(_vaDisplay, _vaSurfaces, _vaSurfaces.Length); + _vaSurfaces = Array.Empty(); + } + + if (_vaConfigId != 0) + { + vaDestroyConfig(_vaDisplay, _vaConfigId); + _vaConfigId = 0; + } + } + } + } + + #endregion + + #region Frame Retrieval + + /// + /// Retrieves a decoded frame from the specified surface. + /// + public VideoFrame? GetDecodedFrame(int surfaceIndex, long timestamp, bool isKeyFrame) + { + if (!_initialized || _currentApi != VideoAccelerationApi.VaApi) + return null; + + if (surfaceIndex < 0 || surfaceIndex >= _vaSurfaces.Length) + return null; + + uint surfaceId = _vaSurfaces[surfaceIndex]; + + // Wait for decode to complete + int status = vaSyncSurface(_vaDisplay, surfaceId); + if (status != VA_STATUS_SUCCESS) + return null; + + // Derive image from surface + status = vaDeriveImage(_vaDisplay, surfaceId, out VaImage image); + if (status != VA_STATUS_SUCCESS) + return null; + + // Map the buffer + status = vaMapBuffer(_vaDisplay, image.BufferId, out IntPtr data); + if (status != VA_STATUS_SUCCESS) + { + vaDestroyImage(_vaDisplay, image.ImageId); + return null; + } + + var frame = new VideoFrame + { + Width = image.Width, + Height = image.Height, + DataY = data + (int)image.OffsetsPlane0, + DataU = data + (int)image.OffsetsPlane1, + DataV = data + (int)image.OffsetsPlane2, + StrideY = (int)image.PitchesPlane0, + StrideU = (int)image.PitchesPlane1, + StrideV = (int)image.PitchesPlane2, + Timestamp = timestamp, + IsKeyFrame = isKeyFrame + }; + + // Set cleanup callback + frame.SetReleaseCallback(() => + { + vaUnmapBuffer(_vaDisplay, image.BufferId); + vaDestroyImage(_vaDisplay, image.ImageId); + }); + + return frame; + } + + /// + /// Converts a decoded frame to an SKBitmap for display. + /// + public SKBitmap? ConvertFrameToSkia(VideoFrame frame) + { + if (frame == null) + return null; + + // Create BGRA bitmap + var bitmap = new SKBitmap(frame.Width, frame.Height, SKColorType.Bgra8888, SKAlphaType.Opaque); + + // Convert YUV to BGRA + unsafe + { + byte* yPtr = (byte*)frame.DataY; + byte* uPtr = (byte*)frame.DataU; + byte* vPtr = (byte*)frame.DataV; + byte* dst = (byte*)bitmap.GetPixels(); + + for (int y = 0; y < frame.Height; y++) + { + for (int x = 0; x < frame.Width; x++) + { + int yIndex = y * frame.StrideY + x; + int uvIndex = (y / 2) * frame.StrideU + (x / 2); + + int yVal = yPtr[yIndex]; + int uVal = uPtr[uvIndex] - 128; + int vVal = vPtr[uvIndex] - 128; + + // YUV to RGB conversion + int r = (int)(yVal + 1.402 * vVal); + int g = (int)(yVal - 0.344 * uVal - 0.714 * vVal); + int b = (int)(yVal + 1.772 * uVal); + + r = Math.Clamp(r, 0, 255); + g = Math.Clamp(g, 0, 255); + b = Math.Clamp(b, 0, 255); + + int dstIndex = (y * frame.Width + x) * 4; + dst[dstIndex] = (byte)b; + dst[dstIndex + 1] = (byte)g; + dst[dstIndex + 2] = (byte)r; + dst[dstIndex + 3] = 255; + } + } + } + + return bitmap; + } + + #endregion + + #region Helpers + + private static bool TryMapVaProfile(int vaProfile, out VideoProfile profile) + { + profile = vaProfile switch + { + VAProfileH264Baseline => VideoProfile.H264Baseline, + VAProfileH264Main => VideoProfile.H264Main, + VAProfileH264High => VideoProfile.H264High, + VAProfileHEVCMain => VideoProfile.H265Main, + VAProfileHEVCMain10 => VideoProfile.H265Main10, + VAProfileVP8Version0_3 => VideoProfile.Vp8, + VAProfileVP9Profile0 => VideoProfile.Vp9Profile0, + VAProfileVP9Profile2 => VideoProfile.Vp9Profile2, + VAProfileAV1Profile0 => VideoProfile.Av1Main, + _ => VideoProfile.H264Main + }; + + return vaProfile >= VAProfileH264Baseline && vaProfile <= VAProfileAV1Profile0; + } + + private static int MapToVaProfile(VideoProfile profile) + { + return profile switch + { + VideoProfile.H264Baseline => VAProfileH264Baseline, + VideoProfile.H264Main => VAProfileH264Main, + VideoProfile.H264High => VAProfileH264High, + VideoProfile.H265Main => VAProfileHEVCMain, + VideoProfile.H265Main10 => VAProfileHEVCMain10, + VideoProfile.Vp8 => VAProfileVP8Version0_3, + VideoProfile.Vp9Profile0 => VAProfileVP9Profile0, + VideoProfile.Vp9Profile2 => VAProfileVP9Profile2, + VideoProfile.Av1Main => VAProfileAV1Profile0, + _ => VAProfileH264Main + }; + } + + private static string GetVaError(int status) + { + try + { + IntPtr errPtr = vaErrorStr(status); + return Marshal.PtrToStringAnsi(errPtr) ?? $"Unknown error {status}"; + } + catch + { + return $"Error code {status}"; + } + } + + #endregion + + #region IDisposable + + public void Dispose() + { + if (_disposed) + return; + + _disposed = true; + + DestroyDecoder(); + + if (_currentApi == VideoAccelerationApi.VaApi && _vaDisplay != IntPtr.Zero) + { + vaTerminate(_vaDisplay); + _vaDisplay = IntPtr.Zero; + } + + if (_drmFd >= 0) + { + close(_drmFd); + _drmFd = -1; + } + + GC.SuppressFinalize(this); + } + + ~HardwareVideoService() + { + Dispose(); + } + + #endregion } diff --git a/Services/HiDpiService.cs b/Services/HiDpiService.cs index b5ceeca..c48b8f6 100644 --- a/Services/HiDpiService.cs +++ b/Services/HiDpiService.cs @@ -1,387 +1,524 @@ -using System; -using System.Diagnostics; -using System.IO; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using System.Runtime.InteropServices; using System.Text.RegularExpressions; namespace Microsoft.Maui.Platform.Linux.Services; +/// +/// Provides HiDPI and display scaling detection for Linux. +/// public class HiDpiService { - private const float DefaultDpi = 96f; + private const float DefaultDpi = 96f; + private float _scaleFactor = 1.0f; + private float _dpi = DefaultDpi; + private bool _initialized; - private float _scaleFactor = 1f; + /// + /// Gets the current scale factor. + /// + public float ScaleFactor => _scaleFactor; - private float _dpi = 96f; + /// + /// Gets the current DPI. + /// + public float Dpi => _dpi; - private bool _initialized; + /// + /// Event raised when scale factor changes. + /// + public event EventHandler? ScaleChanged; - public float ScaleFactor => _scaleFactor; + /// + /// Initializes the HiDPI detection service. + /// + public void Initialize() + { + if (_initialized) return; + _initialized = true; - public float Dpi => _dpi; + DetectScaleFactor(); + } - public event EventHandler? ScaleChanged; + /// + /// Detects the current scale factor using multiple methods. + /// + public void DetectScaleFactor() + { + float scale = 1.0f; + float dpi = DefaultDpi; - public void Initialize() - { - if (!_initialized) - { - _initialized = true; - DetectScaleFactor(); - } - } + // Try multiple detection methods in order of preference + if (TryGetEnvironmentScale(out float envScale)) + { + scale = envScale; + } + else if (TryGetGnomeScale(out float gnomeScale, out float gnomeDpi)) + { + scale = gnomeScale; + dpi = gnomeDpi; + } + else if (TryGetKdeScale(out float kdeScale)) + { + scale = kdeScale; + } + else if (TryGetX11Scale(out float x11Scale, out float x11Dpi)) + { + scale = x11Scale; + dpi = x11Dpi; + } + else if (TryGetXrandrScale(out float xrandrScale)) + { + scale = xrandrScale; + } - public void DetectScaleFactor() - { - float scale = 1f; - float dpi = 96f; - float scale3; - float dpi2; - float scale4; - float scale5; - float dpi3; - float scale6; - if (TryGetEnvironmentScale(out var scale2)) - { - scale = scale2; - } - else if (TryGetGnomeScale(out scale3, out dpi2)) - { - scale = scale3; - dpi = dpi2; - } - else if (TryGetKdeScale(out scale4)) - { - scale = scale4; - } - else if (TryGetX11Scale(out scale5, out dpi3)) - { - scale = scale5; - dpi = dpi3; - } - else if (TryGetXrandrScale(out scale6)) - { - scale = scale6; - } - UpdateScale(scale, dpi); - } + UpdateScale(scale, dpi); + } - private void UpdateScale(float scale, float dpi) - { - if (Math.Abs(_scaleFactor - scale) > 0.01f || Math.Abs(_dpi - dpi) > 0.01f) - { - float scaleFactor = _scaleFactor; - _scaleFactor = scale; - _dpi = dpi; - this.ScaleChanged?.Invoke(this, new ScaleChangedEventArgs(scaleFactor, scale, dpi)); - } - } + private void UpdateScale(float scale, float dpi) + { + if (Math.Abs(_scaleFactor - scale) > 0.01f || Math.Abs(_dpi - dpi) > 0.01f) + { + var oldScale = _scaleFactor; + _scaleFactor = scale; + _dpi = dpi; + ScaleChanged?.Invoke(this, new ScaleChangedEventArgs(oldScale, scale, dpi)); + } + } - private static bool TryGetEnvironmentScale(out float scale) - { - scale = 1f; - string environmentVariable = Environment.GetEnvironmentVariable("GDK_SCALE"); - if (!string.IsNullOrEmpty(environmentVariable) && float.TryParse(environmentVariable, out var result)) - { - scale = result; - return true; - } - string environmentVariable2 = Environment.GetEnvironmentVariable("GDK_DPI_SCALE"); - if (!string.IsNullOrEmpty(environmentVariable2) && float.TryParse(environmentVariable2, out var result2)) - { - scale = result2; - return true; - } - string environmentVariable3 = Environment.GetEnvironmentVariable("QT_SCALE_FACTOR"); - if (!string.IsNullOrEmpty(environmentVariable3) && float.TryParse(environmentVariable3, out var result3)) - { - scale = result3; - return true; - } - string environmentVariable4 = Environment.GetEnvironmentVariable("QT_SCREEN_SCALE_FACTORS"); - if (!string.IsNullOrEmpty(environmentVariable4)) - { - string text = environmentVariable4.Split(';')[0]; - if (text.Contains('=')) - { - text = text.Split('=')[1]; - } - if (float.TryParse(text, out var result4)) - { - scale = result4; - return true; - } - } - return false; - } + /// + /// Gets scale from environment variables. + /// + private static bool TryGetEnvironmentScale(out float scale) + { + scale = 1.0f; - private static bool TryGetGnomeScale(out float scale, out float dpi) - { - scale = 1f; - dpi = 96f; - try - { - string text = RunCommand("gsettings", "get org.gnome.desktop.interface scaling-factor"); - if (!string.IsNullOrEmpty(text)) - { - Match match = Regex.Match(text, "uint32\\s+(\\d+)"); - if (match.Success && int.TryParse(match.Groups[1].Value, out var result) && result > 0) - { - scale = result; - } - } - text = RunCommand("gsettings", "get org.gnome.desktop.interface text-scaling-factor"); - if (!string.IsNullOrEmpty(text) && float.TryParse(text.Trim(), out var result2) && result2 > 0.5f) - { - scale = Math.Max(scale, result2); - } - text = RunCommand("gsettings", "get org.gnome.mutter experimental-features"); - if (text != null && text.Contains("scale-monitor-framebuffer")) - { - text = RunCommand("gdbus", "call --session --dest org.gnome.Mutter.DisplayConfig --object-path /org/gnome/Mutter/DisplayConfig --method org.gnome.Mutter.DisplayConfig.GetCurrentState"); - if (text != null) - { - Match match2 = Regex.Match(text, "'scale':\\s*<(\\d+\\.?\\d*)>"); - if (match2.Success && float.TryParse(match2.Groups[1].Value, out var result3)) - { - scale = result3; - } - } - } - return scale > 1f || Math.Abs(scale - 1f) < 0.01f; - } - catch - { - return false; - } - } + // GDK_SCALE (GTK3/4) + var gdkScale = Environment.GetEnvironmentVariable("GDK_SCALE"); + if (!string.IsNullOrEmpty(gdkScale) && float.TryParse(gdkScale, out float gdk)) + { + scale = gdk; + return true; + } - private static bool TryGetKdeScale(out float scale) - { - scale = 1f; - try - { - string text = RunCommand("kreadconfig5", "--file kdeglobals --group KScreen --key ScaleFactor"); - if (!string.IsNullOrEmpty(text) && float.TryParse(text.Trim(), out var result) && result > 0f) - { - scale = result; - return true; - } - text = RunCommand("kreadconfig6", "--file kdeglobals --group KScreen --key ScaleFactor"); - if (!string.IsNullOrEmpty(text) && float.TryParse(text.Trim(), out var result2) && result2 > 0f) - { - scale = result2; - return true; - } - string path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "kdeglobals"); - if (File.Exists(path)) - { - string[] array = File.ReadAllLines(path); - bool flag = false; - string[] array2 = array; - foreach (string text2 in array2) - { - if (text2.Trim() == "[KScreen]") - { - flag = true; - continue; - } - if (flag && text2.StartsWith("[")) - { - break; - } - if (flag && text2.StartsWith("ScaleFactor=") && float.TryParse(text2.Substring("ScaleFactor=".Length), out var result3)) - { - scale = result3; - return true; - } - } - } - return false; - } - catch - { - return false; - } - } + // GDK_DPI_SCALE (GTK3/4) + var gdkDpiScale = Environment.GetEnvironmentVariable("GDK_DPI_SCALE"); + if (!string.IsNullOrEmpty(gdkDpiScale) && float.TryParse(gdkDpiScale, out float gdkDpi)) + { + scale = gdkDpi; + return true; + } - private bool TryGetX11Scale(out float scale, out float dpi) - { - scale = 1f; - dpi = 96f; - try - { - string text = RunCommand("xrdb", "-query"); - if (!string.IsNullOrEmpty(text)) - { - Match match = Regex.Match(text, "Xft\\.dpi:\\s*(\\d+)"); - if (match.Success && float.TryParse(match.Groups[1].Value, out var result)) - { - dpi = result; - scale = result / 96f; - return true; - } - } - string path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".Xresources"); - if (File.Exists(path)) - { - Match match2 = Regex.Match(File.ReadAllText(path), "Xft\\.dpi:\\s*(\\d+)"); - if (match2.Success && float.TryParse(match2.Groups[1].Value, out var result2)) - { - dpi = result2; - scale = result2 / 96f; - return true; - } - } - return TryGetX11DpiDirect(out scale, out dpi); - } - catch - { - return false; - } - } + // QT_SCALE_FACTOR + var qtScale = Environment.GetEnvironmentVariable("QT_SCALE_FACTOR"); + if (!string.IsNullOrEmpty(qtScale) && float.TryParse(qtScale, out float qt)) + { + scale = qt; + return true; + } - private bool TryGetX11DpiDirect(out float scale, out float dpi) - { - scale = 1f; - dpi = 96f; - try - { - IntPtr intPtr = XOpenDisplay(IntPtr.Zero); - if (intPtr == IntPtr.Zero) - { - return false; - } - try - { - int screen = XDefaultScreen(intPtr); - int num = XDisplayWidthMM(intPtr, screen); - int num2 = XDisplayHeightMM(intPtr, screen); - int num3 = XDisplayWidth(intPtr, screen); - int num4 = XDisplayHeight(intPtr, screen); - if (num > 0 && num2 > 0) - { - float num5 = (float)num3 * 25.4f / (float)num; - float num6 = (float)num4 * 25.4f / (float)num2; - dpi = (num5 + num6) / 2f; - scale = dpi / 96f; - return true; - } - return false; - } - finally - { - XCloseDisplay(intPtr); - } - } - catch - { - return false; - } - } + // QT_SCREEN_SCALE_FACTORS (can be per-screen) + var qtScreenScales = Environment.GetEnvironmentVariable("QT_SCREEN_SCALE_FACTORS"); + if (!string.IsNullOrEmpty(qtScreenScales)) + { + // Format: "screen1=1.5;screen2=2.0" or just "1.5" + var first = qtScreenScales.Split(';')[0]; + if (first.Contains('=')) + { + first = first.Split('=')[1]; + } + if (float.TryParse(first, out float qtScreen)) + { + scale = qtScreen; + return true; + } + } - private static bool TryGetXrandrScale(out float scale) - { - scale = 1f; - try - { - string text = RunCommand("xrandr", "--query"); - if (string.IsNullOrEmpty(text)) - { - return false; - } - string[] array = text.Split('\n'); - foreach (string text2 in array) - { - if (text2.Contains("connected") && !text2.Contains("disconnected")) - { - Match match = Regex.Match(text2, "(\\d+)x(\\d+)\\+\\d+\\+\\d+"); - Match match2 = Regex.Match(text2, "(\\d+)mm x (\\d+)mm"); - if (match.Success && match2.Success && int.TryParse(match.Groups[1].Value, out var result) && int.TryParse(match2.Groups[1].Value, out var result2) && result2 > 0) - { - float num = (float)result * 25.4f / (float)result2; - scale = num / 96f; - return true; - } - } - } - return false; - } - catch - { - return false; - } - } + return false; + } - private static string? RunCommand(string command, string arguments) - { - try - { - using Process process = new Process(); - process.StartInfo = new ProcessStartInfo - { - FileName = command, - Arguments = arguments, - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - CreateNoWindow = true - }; - process.Start(); - string result = process.StandardOutput.ReadToEnd(); - process.WaitForExit(1000); - return result; - } - catch - { - return null; - } - } + /// + /// Gets scale from GNOME settings. + /// + private static bool TryGetGnomeScale(out float scale, out float dpi) + { + scale = 1.0f; + dpi = DefaultDpi; - public float ToPhysicalPixels(float logicalPixels) - { - return logicalPixels * _scaleFactor; - } + try + { + // Try gsettings for GNOME + var result = RunCommand("gsettings", "get org.gnome.desktop.interface scaling-factor"); + if (!string.IsNullOrEmpty(result)) + { + var match = Regex.Match(result, @"uint32\s+(\d+)"); + if (match.Success && int.TryParse(match.Groups[1].Value, out int gnomeScale)) + { + if (gnomeScale > 0) + { + scale = gnomeScale; + } + } + } - public float ToLogicalPixels(float physicalPixels) - { - return physicalPixels / _scaleFactor; - } + // Also check text-scaling-factor for fractional scaling + result = RunCommand("gsettings", "get org.gnome.desktop.interface text-scaling-factor"); + if (!string.IsNullOrEmpty(result) && float.TryParse(result.Trim(), out float textScale)) + { + if (textScale > 0.5f) + { + scale = Math.Max(scale, textScale); + } + } - public float GetFontScaleFactor() - { - try - { - string text = RunCommand("gsettings", "get org.gnome.desktop.interface text-scaling-factor"); - if (!string.IsNullOrEmpty(text) && float.TryParse(text.Trim(), out var result)) - { - return result; - } - } - catch - { - } - return _scaleFactor; - } + // Check for GNOME 40+ experimental fractional scaling + result = RunCommand("gsettings", "get org.gnome.mutter experimental-features"); + if (result != null && result.Contains("scale-monitor-framebuffer")) + { + // Fractional scaling is enabled, try to get actual scale + result = RunCommand("gdbus", "call --session --dest org.gnome.Mutter.DisplayConfig --object-path /org/gnome/Mutter/DisplayConfig --method org.gnome.Mutter.DisplayConfig.GetCurrentState"); + if (result != null) + { + // Parse for scale value + var scaleMatch = Regex.Match(result, @"'scale':\s*<(\d+\.?\d*)>"); + if (scaleMatch.Success && float.TryParse(scaleMatch.Groups[1].Value, out float mutterScale)) + { + scale = mutterScale; + } + } + } - [DllImport("libX11.so.6")] - private static extern IntPtr XOpenDisplay(IntPtr display); + return scale > 1.0f || Math.Abs(scale - 1.0f) < 0.01f; + } + catch + { + return false; + } + } - [DllImport("libX11.so.6")] - private static extern void XCloseDisplay(IntPtr display); + /// + /// Gets scale from KDE settings. + /// + private static bool TryGetKdeScale(out float scale) + { + scale = 1.0f; - [DllImport("libX11.so.6")] - private static extern int XDefaultScreen(IntPtr display); + try + { + // Try kreadconfig5 for KDE Plasma 5 + var result = RunCommand("kreadconfig5", "--file kdeglobals --group KScreen --key ScaleFactor"); + if (!string.IsNullOrEmpty(result) && float.TryParse(result.Trim(), out float kdeScale)) + { + if (kdeScale > 0) + { + scale = kdeScale; + return true; + } + } - [DllImport("libX11.so.6")] - private static extern int XDisplayWidth(IntPtr display, int screen); + // Try KDE Plasma 6 + result = RunCommand("kreadconfig6", "--file kdeglobals --group KScreen --key ScaleFactor"); + if (!string.IsNullOrEmpty(result) && float.TryParse(result.Trim(), out float kde6Scale)) + { + if (kde6Scale > 0) + { + scale = kde6Scale; + return true; + } + } - [DllImport("libX11.so.6")] - private static extern int XDisplayHeight(IntPtr display, int screen); + // Check kdeglobals config file directly + var configPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + ".config", "kdeglobals"); - [DllImport("libX11.so.6")] - private static extern int XDisplayWidthMM(IntPtr display, int screen); + if (File.Exists(configPath)) + { + var lines = File.ReadAllLines(configPath); + bool inKScreenSection = false; + foreach (var line in lines) + { + if (line.Trim() == "[KScreen]") + { + inKScreenSection = true; + continue; + } + if (inKScreenSection && line.StartsWith("[")) + { + break; + } + if (inKScreenSection && line.StartsWith("ScaleFactor=")) + { + var value = line.Substring("ScaleFactor=".Length); + if (float.TryParse(value, out float fileScale)) + { + scale = fileScale; + return true; + } + } + } + } - [DllImport("libX11.so.6")] - private static extern int XDisplayHeightMM(IntPtr display, int screen); + return false; + } + catch + { + return false; + } + } + + /// + /// Gets scale from X11 Xresources. + /// + private bool TryGetX11Scale(out float scale, out float dpi) + { + scale = 1.0f; + dpi = DefaultDpi; + + try + { + // Try xrdb query + var result = RunCommand("xrdb", "-query"); + if (!string.IsNullOrEmpty(result)) + { + // Look for Xft.dpi + var match = Regex.Match(result, @"Xft\.dpi:\s*(\d+)"); + if (match.Success && float.TryParse(match.Groups[1].Value, out float xftDpi)) + { + dpi = xftDpi; + scale = xftDpi / DefaultDpi; + return true; + } + } + + // Try reading .Xresources directly + var xresourcesPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + ".Xresources"); + + if (File.Exists(xresourcesPath)) + { + var content = File.ReadAllText(xresourcesPath); + var match = Regex.Match(content, @"Xft\.dpi:\s*(\d+)"); + if (match.Success && float.TryParse(match.Groups[1].Value, out float fileDpi)) + { + dpi = fileDpi; + scale = fileDpi / DefaultDpi; + return true; + } + } + + // Try X11 directly + return TryGetX11DpiDirect(out scale, out dpi); + } + catch + { + return false; + } + } + + /// + /// Gets DPI directly from X11 server. + /// + private bool TryGetX11DpiDirect(out float scale, out float dpi) + { + scale = 1.0f; + dpi = DefaultDpi; + + try + { + var display = XOpenDisplay(IntPtr.Zero); + if (display == IntPtr.Zero) return false; + + try + { + int screen = XDefaultScreen(display); + + // Get physical dimensions + int widthMm = XDisplayWidthMM(display, screen); + int heightMm = XDisplayHeightMM(display, screen); + int widthPx = XDisplayWidth(display, screen); + int heightPx = XDisplayHeight(display, screen); + + if (widthMm > 0 && heightMm > 0) + { + float dpiX = widthPx * 25.4f / widthMm; + float dpiY = heightPx * 25.4f / heightMm; + dpi = (dpiX + dpiY) / 2; + scale = dpi / DefaultDpi; + return true; + } + + return false; + } + finally + { + XCloseDisplay(display); + } + } + catch + { + return false; + } + } + + /// + /// Gets scale from xrandr output. + /// + private static bool TryGetXrandrScale(out float scale) + { + scale = 1.0f; + + try + { + var result = RunCommand("xrandr", "--query"); + if (string.IsNullOrEmpty(result)) return false; + + // Look for connected displays with scaling + // Format: "eDP-1 connected primary 2560x1440+0+0 (normal left inverted right x axis y axis) 309mm x 174mm" + var lines = result.Split('\n'); + foreach (var line in lines) + { + if (!line.Contains("connected") || line.Contains("disconnected")) continue; + + // Try to find resolution and physical size + var resMatch = Regex.Match(line, @"(\d+)x(\d+)\+\d+\+\d+"); + var mmMatch = Regex.Match(line, @"(\d+)mm x (\d+)mm"); + + if (resMatch.Success && mmMatch.Success) + { + if (int.TryParse(resMatch.Groups[1].Value, out int widthPx) && + int.TryParse(mmMatch.Groups[1].Value, out int widthMm) && + widthMm > 0) + { + float dpi = widthPx * 25.4f / widthMm; + scale = dpi / DefaultDpi; + return true; + } + } + } + + return false; + } + catch + { + return false; + } + } + + private static string? RunCommand(string command, string arguments) + { + try + { + using var process = new System.Diagnostics.Process(); + process.StartInfo = new System.Diagnostics.ProcessStartInfo + { + FileName = command, + Arguments = arguments, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + process.Start(); + var output = process.StandardOutput.ReadToEnd(); + process.WaitForExit(1000); + return output; + } + catch + { + return null; + } + } + + /// + /// Converts logical pixels to physical pixels. + /// + public float ToPhysicalPixels(float logicalPixels) + { + return logicalPixels * _scaleFactor; + } + + /// + /// Converts physical pixels to logical pixels. + /// + public float ToLogicalPixels(float physicalPixels) + { + return physicalPixels / _scaleFactor; + } + + /// + /// Gets the recommended font scale factor. + /// + public float GetFontScaleFactor() + { + // Some desktop environments use a separate text scaling factor + try + { + var result = RunCommand("gsettings", "get org.gnome.desktop.interface text-scaling-factor"); + if (!string.IsNullOrEmpty(result) && float.TryParse(result.Trim(), out float textScale)) + { + return textScale; + } + } + catch { } + + return _scaleFactor; + } + + #region X11 Interop + + [DllImport("libX11.so.6")] + private static extern nint XOpenDisplay(nint display); + + [DllImport("libX11.so.6")] + private static extern void XCloseDisplay(nint display); + + [DllImport("libX11.so.6")] + private static extern int XDefaultScreen(nint display); + + [DllImport("libX11.so.6")] + private static extern int XDisplayWidth(nint display, int screen); + + [DllImport("libX11.so.6")] + private static extern int XDisplayHeight(nint display, int screen); + + [DllImport("libX11.so.6")] + private static extern int XDisplayWidthMM(nint display, int screen); + + [DllImport("libX11.so.6")] + private static extern int XDisplayHeightMM(nint display, int screen); + + #endregion +} + +/// +/// Event args for scale change events. +/// +public class ScaleChangedEventArgs : EventArgs +{ + /// + /// Gets the old scale factor. + /// + public float OldScale { get; } + + /// + /// Gets the new scale factor. + /// + public float NewScale { get; } + + /// + /// Gets the new DPI. + /// + public float NewDpi { get; } + + public ScaleChangedEventArgs(float oldScale, float newScale, float newDpi) + { + OldScale = oldScale; + NewScale = newScale; + NewDpi = newDpi; + } } diff --git a/Services/HighContrastChangedEventArgs.cs b/Services/HighContrastChangedEventArgs.cs deleted file mode 100644 index 82fed01..0000000 --- a/Services/HighContrastChangedEventArgs.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; - -namespace Microsoft.Maui.Platform.Linux.Services; - -public class HighContrastChangedEventArgs : EventArgs -{ - public bool IsEnabled { get; } - - public HighContrastTheme Theme { get; } - - public HighContrastChangedEventArgs(bool isEnabled, HighContrastTheme theme) - { - IsEnabled = isEnabled; - Theme = theme; - } -} diff --git a/Services/HighContrastColors.cs b/Services/HighContrastColors.cs deleted file mode 100644 index 515a209..0000000 --- a/Services/HighContrastColors.cs +++ /dev/null @@ -1,32 +0,0 @@ -using SkiaSharp; - -namespace Microsoft.Maui.Platform.Linux.Services; - -public class HighContrastColors -{ - public SKColor Background { get; set; } - - public SKColor Foreground { get; set; } - - public SKColor Accent { get; set; } - - public SKColor Border { get; set; } - - public SKColor Error { get; set; } - - public SKColor Success { get; set; } - - public SKColor Warning { get; set; } - - public SKColor Link { get; set; } - - public SKColor LinkVisited { get; set; } - - public SKColor Selection { get; set; } - - public SKColor SelectionText { get; set; } - - public SKColor DisabledText { get; set; } - - public SKColor DisabledBackground { get; set; } -} diff --git a/Services/HighContrastService.cs b/Services/HighContrastService.cs index 91fb490..3e3fd64 100644 --- a/Services/HighContrastService.cs +++ b/Services/HighContrastService.cs @@ -1,342 +1,402 @@ -using System; -using System.Diagnostics; -using System.IO; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using SkiaSharp; namespace Microsoft.Maui.Platform.Linux.Services; +/// +/// Provides high contrast mode detection and theme support for accessibility. +/// public class HighContrastService { - private bool _isHighContrastEnabled; + private bool _isHighContrastEnabled; + private HighContrastTheme _currentTheme = HighContrastTheme.None; + private bool _initialized; - private HighContrastTheme _currentTheme; + /// + /// Gets whether high contrast mode is enabled. + /// + public bool IsHighContrastEnabled => _isHighContrastEnabled; - private bool _initialized; + /// + /// Gets the current high contrast theme. + /// + public HighContrastTheme CurrentTheme => _currentTheme; - public bool IsHighContrastEnabled => _isHighContrastEnabled; + /// + /// Event raised when high contrast mode changes. + /// + public event EventHandler? HighContrastChanged; - public HighContrastTheme CurrentTheme => _currentTheme; + /// + /// Initializes the high contrast service. + /// + public void Initialize() + { + if (_initialized) return; + _initialized = true; - public event EventHandler? HighContrastChanged; + DetectHighContrast(); + } - public void Initialize() - { - if (!_initialized) - { - _initialized = true; - DetectHighContrast(); - } - } + /// + /// Detects current high contrast mode settings. + /// + public void DetectHighContrast() + { + bool isEnabled = false; + var theme = HighContrastTheme.None; - public void DetectHighContrast() - { - bool isEnabled = false; - HighContrastTheme theme = HighContrastTheme.None; - bool isEnabled3; - string themeName2; - bool isEnabled4; - string themeName3; - bool isEnabled5; - if (TryGetGnomeHighContrast(out bool isEnabled2, out string themeName)) - { - isEnabled = isEnabled2; - if (isEnabled2) - { - theme = ParseThemeName(themeName); - } - } - else if (TryGetKdeHighContrast(out isEnabled3, out themeName2)) - { - isEnabled = isEnabled3; - if (isEnabled3) - { - theme = ParseThemeName(themeName2); - } - } - else if (TryGetGtkHighContrast(out isEnabled4, out themeName3)) - { - isEnabled = isEnabled4; - if (isEnabled4) - { - theme = ParseThemeName(themeName3); - } - } - else if (TryGetEnvironmentHighContrast(out isEnabled5)) - { - isEnabled = isEnabled5; - theme = HighContrastTheme.WhiteOnBlack; - } - UpdateHighContrast(isEnabled, theme); - } + // Try GNOME settings + if (TryGetGnomeHighContrast(out bool gnomeEnabled, out string? gnomeTheme)) + { + isEnabled = gnomeEnabled; + if (gnomeEnabled) + { + theme = ParseThemeName(gnomeTheme); + } + } + // Try KDE settings + else if (TryGetKdeHighContrast(out bool kdeEnabled, out string? kdeTheme)) + { + isEnabled = kdeEnabled; + if (kdeEnabled) + { + theme = ParseThemeName(kdeTheme); + } + } + // Try GTK settings + else if (TryGetGtkHighContrast(out bool gtkEnabled, out string? gtkTheme)) + { + isEnabled = gtkEnabled; + if (gtkEnabled) + { + theme = ParseThemeName(gtkTheme); + } + } + // Check environment variables + else if (TryGetEnvironmentHighContrast(out bool envEnabled)) + { + isEnabled = envEnabled; + theme = HighContrastTheme.WhiteOnBlack; // Default + } - private void UpdateHighContrast(bool isEnabled, HighContrastTheme theme) - { - if (_isHighContrastEnabled != isEnabled || _currentTheme != theme) - { - _isHighContrastEnabled = isEnabled; - _currentTheme = theme; - this.HighContrastChanged?.Invoke(this, new HighContrastChangedEventArgs(isEnabled, theme)); - } - } + UpdateHighContrast(isEnabled, theme); + } - private static bool TryGetGnomeHighContrast(out bool isEnabled, out string? themeName) - { - isEnabled = false; - themeName = null; - try - { - string text = RunCommand("gsettings", "get org.gnome.desktop.a11y.interface high-contrast"); - if (!string.IsNullOrEmpty(text)) - { - isEnabled = text.Trim().ToLower() == "true"; - } - text = RunCommand("gsettings", "get org.gnome.desktop.interface gtk-theme"); - if (!string.IsNullOrEmpty(text)) - { - themeName = text.Trim().Trim('\''); - if (!isEnabled && themeName != null) - { - string text2 = themeName.ToLower(); - isEnabled = text2.Contains("highcontrast") || text2.Contains("high-contrast") || text2.Contains("hc"); - } - } - return true; - } - catch - { - return false; - } - } + private void UpdateHighContrast(bool isEnabled, HighContrastTheme theme) + { + if (_isHighContrastEnabled != isEnabled || _currentTheme != theme) + { + _isHighContrastEnabled = isEnabled; + _currentTheme = theme; + HighContrastChanged?.Invoke(this, new HighContrastChangedEventArgs(isEnabled, theme)); + } + } - private static bool TryGetKdeHighContrast(out bool isEnabled, out string? themeName) - { - isEnabled = false; - themeName = null; - try - { - string path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "kdeglobals"); - if (!File.Exists(path)) - { - return false; - } - string[] array = File.ReadAllLines(path); - foreach (string text in array) - { - if (text.StartsWith("ColorScheme=")) - { - themeName = text.Substring("ColorScheme=".Length); - string text2 = themeName.ToLower(); - isEnabled = text2.Contains("highcontrast") || text2.Contains("high-contrast") || text2.Contains("breeze-high-contrast"); - return true; - } - } - return false; - } - catch - { - return false; - } - } + private static bool TryGetGnomeHighContrast(out bool isEnabled, out string? themeName) + { + isEnabled = false; + themeName = null; - private static bool TryGetGtkHighContrast(out bool isEnabled, out string? themeName) - { - isEnabled = false; - themeName = null; - try - { - string path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "gtk-3.0", "settings.ini"); - if (!File.Exists(path)) - { - path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "gtk-4.0", "settings.ini"); - } - if (!File.Exists(path)) - { - return false; - } - string[] array = File.ReadAllLines(path); - foreach (string text in array) - { - if (text.StartsWith("gtk-theme-name=")) - { - themeName = text.Substring("gtk-theme-name=".Length); - string text2 = themeName.ToLower(); - isEnabled = text2.Contains("highcontrast") || text2.Contains("high-contrast"); - return true; - } - } - return false; - } - catch - { - return false; - } - } + try + { + // Check if high contrast is enabled via gsettings + var result = RunCommand("gsettings", "get org.gnome.desktop.a11y.interface high-contrast"); + if (!string.IsNullOrEmpty(result)) + { + isEnabled = result.Trim().ToLower() == "true"; + } - private static bool TryGetEnvironmentHighContrast(out bool isEnabled) - { - isEnabled = false; - string environmentVariable = Environment.GetEnvironmentVariable("GTK_THEME"); - if (!string.IsNullOrEmpty(environmentVariable)) - { - string text = environmentVariable.ToLower(); - isEnabled = text.Contains("highcontrast") || text.Contains("high-contrast"); - if (isEnabled) - { - return true; - } - } - string environmentVariable2 = Environment.GetEnvironmentVariable("GTK_A11Y"); - if (!(environmentVariable2?.ToLower() == "atspi")) - { - _ = environmentVariable2 == "1"; - } - return isEnabled; - } + // Get the current GTK theme + result = RunCommand("gsettings", "get org.gnome.desktop.interface gtk-theme"); + if (!string.IsNullOrEmpty(result)) + { + themeName = result.Trim().Trim('\''); - private static HighContrastTheme ParseThemeName(string? themeName) - { - if (string.IsNullOrEmpty(themeName)) - { - return HighContrastTheme.WhiteOnBlack; - } - string text = themeName.ToLower(); - if (text.Contains("inverse") || text.Contains("dark") || text.Contains("white-on-black")) - { - return HighContrastTheme.WhiteOnBlack; - } - if (text.Contains("light") || text.Contains("black-on-white")) - { - return HighContrastTheme.BlackOnWhite; - } - return HighContrastTheme.WhiteOnBlack; - } + // Check if theme name indicates high contrast + if (!isEnabled && themeName != null) + { + var lowerTheme = themeName.ToLower(); + isEnabled = lowerTheme.Contains("highcontrast") || + lowerTheme.Contains("high-contrast") || + lowerTheme.Contains("hc"); + } + } - public HighContrastColors GetColors() - { - //IL_001d: Unknown result type (might be due to invalid IL or missing references) - //IL_0028: Unknown result type (might be due to invalid IL or missing references) - //IL_003e: Unknown result type (might be due to invalid IL or missing references) - //IL_0049: Unknown result type (might be due to invalid IL or missing references) - //IL_005d: Unknown result type (might be due to invalid IL or missing references) - //IL_0071: Unknown result type (might be due to invalid IL or missing references) - //IL_007c: Unknown result type (might be due to invalid IL or missing references) - //IL_0093: Unknown result type (might be due to invalid IL or missing references) - //IL_00ad: Unknown result type (might be due to invalid IL or missing references) - //IL_00c0: Unknown result type (might be due to invalid IL or missing references) - //IL_00cb: Unknown result type (might be due to invalid IL or missing references) - //IL_00e5: Unknown result type (might be due to invalid IL or missing references) - //IL_00f6: Unknown result type (might be due to invalid IL or missing references) - //IL_010c: Unknown result type (might be due to invalid IL or missing references) - //IL_0117: Unknown result type (might be due to invalid IL or missing references) - //IL_0129: Unknown result type (might be due to invalid IL or missing references) - //IL_0134: Unknown result type (might be due to invalid IL or missing references) - //IL_0146: Unknown result type (might be due to invalid IL or missing references) - //IL_0158: Unknown result type (might be due to invalid IL or missing references) - //IL_016b: Unknown result type (might be due to invalid IL or missing references) - //IL_017d: Unknown result type (might be due to invalid IL or missing references) - //IL_018d: Unknown result type (might be due to invalid IL or missing references) - //IL_01a0: Unknown result type (might be due to invalid IL or missing references) - //IL_01ab: Unknown result type (might be due to invalid IL or missing references) - //IL_01bc: Unknown result type (might be due to invalid IL or missing references) - //IL_01d6: Unknown result type (might be due to invalid IL or missing references) - return _currentTheme switch - { - HighContrastTheme.WhiteOnBlack => new HighContrastColors - { - Background = SKColors.Black, - Foreground = SKColors.White, - Accent = new SKColor((byte)0, byte.MaxValue, byte.MaxValue), - Border = SKColors.White, - Error = new SKColor(byte.MaxValue, (byte)100, (byte)100), - Success = new SKColor((byte)100, byte.MaxValue, (byte)100), - Warning = SKColors.Yellow, - Link = new SKColor((byte)100, (byte)200, byte.MaxValue), - LinkVisited = new SKColor((byte)200, (byte)150, byte.MaxValue), - Selection = new SKColor((byte)0, (byte)120, (byte)215), - SelectionText = SKColors.White, - DisabledText = new SKColor((byte)160, (byte)160, (byte)160), - DisabledBackground = new SKColor((byte)40, (byte)40, (byte)40) - }, - HighContrastTheme.BlackOnWhite => new HighContrastColors - { - Background = SKColors.White, - Foreground = SKColors.Black, - Accent = new SKColor((byte)0, (byte)0, (byte)200), - Border = SKColors.Black, - Error = new SKColor((byte)180, (byte)0, (byte)0), - Success = new SKColor((byte)0, (byte)130, (byte)0), - Warning = new SKColor((byte)180, (byte)120, (byte)0), - Link = new SKColor((byte)0, (byte)0, (byte)180), - LinkVisited = new SKColor((byte)80, (byte)0, (byte)120), - Selection = new SKColor((byte)0, (byte)120, (byte)215), - SelectionText = SKColors.White, - DisabledText = new SKColor((byte)100, (byte)100, (byte)100), - DisabledBackground = new SKColor((byte)220, (byte)220, (byte)220) - }, - _ => GetDefaultColors(), - }; - } + return true; + } + catch + { + return false; + } + } - private static HighContrastColors GetDefaultColors() - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - //IL_0017: Unknown result type (might be due to invalid IL or missing references) - //IL_002e: Unknown result type (might be due to invalid IL or missing references) - //IL_0048: Unknown result type (might be due to invalid IL or missing references) - //IL_005c: Unknown result type (might be due to invalid IL or missing references) - //IL_0070: Unknown result type (might be due to invalid IL or missing references) - //IL_0086: Unknown result type (might be due to invalid IL or missing references) - //IL_009d: Unknown result type (might be due to invalid IL or missing references) - //IL_00b4: Unknown result type (might be due to invalid IL or missing references) - //IL_00cb: Unknown result type (might be due to invalid IL or missing references) - //IL_00d6: Unknown result type (might be due to invalid IL or missing references) - //IL_00f0: Unknown result type (might be due to invalid IL or missing references) - //IL_010a: Unknown result type (might be due to invalid IL or missing references) - return new HighContrastColors - { - Background = SKColors.White, - Foreground = new SKColor((byte)33, (byte)33, (byte)33), - Accent = new SKColor((byte)33, (byte)150, (byte)243), - Border = new SKColor((byte)200, (byte)200, (byte)200), - Error = new SKColor((byte)244, (byte)67, (byte)54), - Success = new SKColor((byte)76, (byte)175, (byte)80), - Warning = new SKColor(byte.MaxValue, (byte)152, (byte)0), - Link = new SKColor((byte)33, (byte)150, (byte)243), - LinkVisited = new SKColor((byte)156, (byte)39, (byte)176), - Selection = new SKColor((byte)33, (byte)150, (byte)243), - SelectionText = SKColors.White, - DisabledText = new SKColor((byte)158, (byte)158, (byte)158), - DisabledBackground = new SKColor((byte)238, (byte)238, (byte)238) - }; - } + private static bool TryGetKdeHighContrast(out bool isEnabled, out string? themeName) + { + isEnabled = false; + themeName = null; - public void ForceHighContrast(bool enabled, HighContrastTheme theme = HighContrastTheme.WhiteOnBlack) - { - UpdateHighContrast(enabled, theme); - } + try + { + // Check kdeglobals for color scheme + var configPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + ".config", "kdeglobals"); - private static string? RunCommand(string command, string arguments) - { - try - { - using Process process = new Process(); - process.StartInfo = new ProcessStartInfo - { - FileName = command, - Arguments = arguments, - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - CreateNoWindow = true - }; - process.Start(); - string result = process.StandardOutput.ReadToEnd(); - process.WaitForExit(1000); - return result; - } - catch - { - return null; - } - } + if (!File.Exists(configPath)) return false; + + var lines = File.ReadAllLines(configPath); + foreach (var line in lines) + { + if (line.StartsWith("ColorScheme=")) + { + themeName = line.Substring("ColorScheme=".Length); + var lowerTheme = themeName.ToLower(); + isEnabled = lowerTheme.Contains("highcontrast") || + lowerTheme.Contains("high-contrast") || + lowerTheme.Contains("breeze-high-contrast"); + return true; + } + } + + return false; + } + catch + { + return false; + } + } + + private static bool TryGetGtkHighContrast(out bool isEnabled, out string? themeName) + { + isEnabled = false; + themeName = null; + + try + { + // Check GTK settings.ini + var gtkConfigPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + ".config", "gtk-3.0", "settings.ini"); + + if (!File.Exists(gtkConfigPath)) + { + gtkConfigPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + ".config", "gtk-4.0", "settings.ini"); + } + + if (!File.Exists(gtkConfigPath)) return false; + + var lines = File.ReadAllLines(gtkConfigPath); + foreach (var line in lines) + { + if (line.StartsWith("gtk-theme-name=")) + { + themeName = line.Substring("gtk-theme-name=".Length); + var lowerTheme = themeName.ToLower(); + isEnabled = lowerTheme.Contains("highcontrast") || + lowerTheme.Contains("high-contrast"); + return true; + } + } + + return false; + } + catch + { + return false; + } + } + + private static bool TryGetEnvironmentHighContrast(out bool isEnabled) + { + isEnabled = false; + + // Check GTK_THEME environment variable + var gtkTheme = Environment.GetEnvironmentVariable("GTK_THEME"); + if (!string.IsNullOrEmpty(gtkTheme)) + { + var lower = gtkTheme.ToLower(); + isEnabled = lower.Contains("highcontrast") || lower.Contains("high-contrast"); + if (isEnabled) return true; + } + + // Check accessibility-related env vars + var forceA11y = Environment.GetEnvironmentVariable("GTK_A11Y"); + if (forceA11y?.ToLower() == "atspi" || forceA11y == "1") + { + // A11y is forced, but doesn't necessarily mean high contrast + } + + return isEnabled; + } + + private static HighContrastTheme ParseThemeName(string? themeName) + { + if (string.IsNullOrEmpty(themeName)) + return HighContrastTheme.WhiteOnBlack; + + var lower = themeName.ToLower(); + + if (lower.Contains("inverse") || lower.Contains("dark") || lower.Contains("white-on-black")) + return HighContrastTheme.WhiteOnBlack; + + if (lower.Contains("light") || lower.Contains("black-on-white")) + return HighContrastTheme.BlackOnWhite; + + // Default to white on black (more common high contrast choice) + return HighContrastTheme.WhiteOnBlack; + } + + /// + /// Gets the appropriate colors for the current high contrast theme. + /// + public HighContrastColors GetColors() + { + return _currentTheme switch + { + HighContrastTheme.WhiteOnBlack => new HighContrastColors + { + Background = SKColors.Black, + Foreground = SKColors.White, + Accent = new SKColor(0, 255, 255), // Cyan + Border = SKColors.White, + Error = new SKColor(255, 100, 100), + Success = new SKColor(100, 255, 100), + Warning = SKColors.Yellow, + Link = new SKColor(100, 200, 255), + LinkVisited = new SKColor(200, 150, 255), + Selection = new SKColor(0, 120, 215), + SelectionText = SKColors.White, + DisabledText = new SKColor(160, 160, 160), + DisabledBackground = new SKColor(40, 40, 40) + }, + HighContrastTheme.BlackOnWhite => new HighContrastColors + { + Background = SKColors.White, + Foreground = SKColors.Black, + Accent = new SKColor(0, 0, 200), // Dark blue + Border = SKColors.Black, + Error = new SKColor(180, 0, 0), + Success = new SKColor(0, 130, 0), + Warning = new SKColor(180, 120, 0), + Link = new SKColor(0, 0, 180), + LinkVisited = new SKColor(80, 0, 120), + Selection = new SKColor(0, 120, 215), + SelectionText = SKColors.White, + DisabledText = new SKColor(100, 100, 100), + DisabledBackground = new SKColor(220, 220, 220) + }, + _ => GetDefaultColors() + }; + } + + private static HighContrastColors GetDefaultColors() + { + return new HighContrastColors + { + Background = SKColors.White, + Foreground = new SKColor(33, 33, 33), + Accent = new SKColor(33, 150, 243), + Border = new SKColor(200, 200, 200), + Error = new SKColor(244, 67, 54), + Success = new SKColor(76, 175, 80), + Warning = new SKColor(255, 152, 0), + Link = new SKColor(33, 150, 243), + LinkVisited = new SKColor(156, 39, 176), + Selection = new SKColor(33, 150, 243), + SelectionText = SKColors.White, + DisabledText = new SKColor(158, 158, 158), + DisabledBackground = new SKColor(238, 238, 238) + }; + } + + /// + /// Forces a specific high contrast mode (for testing or user preference override). + /// + public void ForceHighContrast(bool enabled, HighContrastTheme theme = HighContrastTheme.WhiteOnBlack) + { + UpdateHighContrast(enabled, theme); + } + + private static string? RunCommand(string command, string arguments) + { + try + { + using var process = new System.Diagnostics.Process(); + process.StartInfo = new System.Diagnostics.ProcessStartInfo + { + FileName = command, + Arguments = arguments, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + process.Start(); + var output = process.StandardOutput.ReadToEnd(); + process.WaitForExit(1000); + return output; + } + catch + { + return null; + } + } +} + +/// +/// High contrast theme types. +/// +public enum HighContrastTheme +{ + None, + WhiteOnBlack, + BlackOnWhite +} + +/// +/// Color palette for high contrast mode. +/// +public class HighContrastColors +{ + public SKColor Background { get; set; } + public SKColor Foreground { get; set; } + public SKColor Accent { get; set; } + public SKColor Border { get; set; } + public SKColor Error { get; set; } + public SKColor Success { get; set; } + public SKColor Warning { get; set; } + public SKColor Link { get; set; } + public SKColor LinkVisited { get; set; } + public SKColor Selection { get; set; } + public SKColor SelectionText { get; set; } + public SKColor DisabledText { get; set; } + public SKColor DisabledBackground { get; set; } +} + +/// +/// Event args for high contrast mode changes. +/// +public class HighContrastChangedEventArgs : EventArgs +{ + /// + /// Gets whether high contrast mode is enabled. + /// + public bool IsEnabled { get; } + + /// + /// Gets the current theme. + /// + public HighContrastTheme Theme { get; } + + public HighContrastChangedEventArgs(bool isEnabled, HighContrastTheme theme) + { + IsEnabled = isEnabled; + Theme = theme; + } } diff --git a/Services/HighContrastTheme.cs b/Services/HighContrastTheme.cs deleted file mode 100644 index 22f9e01..0000000 --- a/Services/HighContrastTheme.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Microsoft.Maui.Platform.Linux.Services; - -public enum HighContrastTheme -{ - None, - WhiteOnBlack, - BlackOnWhite -} diff --git a/Services/HotkeyEventArgs.cs b/Services/HotkeyEventArgs.cs deleted file mode 100644 index 9d3c33a..0000000 --- a/Services/HotkeyEventArgs.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; - -namespace Microsoft.Maui.Platform.Linux.Services; - -public class HotkeyEventArgs : EventArgs -{ - public int Id { get; } - - public HotkeyKey Key { get; } - - public HotkeyModifiers Modifiers { get; } - - public HotkeyEventArgs(int id, HotkeyKey key, HotkeyModifiers modifiers) - { - Id = id; - Key = key; - Modifiers = modifiers; - } -} diff --git a/Services/HotkeyKey.cs b/Services/HotkeyKey.cs deleted file mode 100644 index 9351bf3..0000000 --- a/Services/HotkeyKey.cs +++ /dev/null @@ -1,76 +0,0 @@ -namespace Microsoft.Maui.Platform.Linux.Services; - -public enum HotkeyKey : uint -{ - A = 97u, - B = 98u, - C = 99u, - D = 100u, - E = 101u, - F = 102u, - G = 103u, - H = 104u, - I = 105u, - J = 106u, - K = 107u, - L = 108u, - M = 109u, - N = 110u, - O = 111u, - P = 112u, - Q = 113u, - R = 114u, - S = 115u, - T = 116u, - U = 117u, - V = 118u, - W = 119u, - X = 120u, - Y = 121u, - Z = 122u, - D0 = 48u, - D1 = 49u, - D2 = 50u, - D3 = 51u, - D4 = 52u, - D5 = 53u, - D6 = 54u, - D7 = 55u, - D8 = 56u, - D9 = 57u, - F1 = 65470u, - F2 = 65471u, - F3 = 65472u, - F4 = 65473u, - F5 = 65474u, - F6 = 65475u, - F7 = 65476u, - F8 = 65477u, - F9 = 65478u, - F10 = 65479u, - F11 = 65480u, - F12 = 65481u, - Escape = 65307u, - Tab = 65289u, - Return = 65293u, - Space = 32u, - BackSpace = 65288u, - Delete = 65535u, - Insert = 65379u, - Home = 65360u, - End = 65367u, - PageUp = 65365u, - PageDown = 65366u, - Left = 65361u, - Up = 65362u, - Right = 65363u, - Down = 65364u, - AudioPlay = 269025044u, - AudioStop = 269025045u, - AudioPrev = 269025046u, - AudioNext = 269025047u, - AudioMute = 269025042u, - AudioRaiseVolume = 269025043u, - AudioLowerVolume = 269025041u, - Print = 65377u -} diff --git a/Services/HotkeyModifiers.cs b/Services/HotkeyModifiers.cs deleted file mode 100644 index 3b1f715..0000000 --- a/Services/HotkeyModifiers.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; - -namespace Microsoft.Maui.Platform.Linux.Services; - -[Flags] -public enum HotkeyModifiers -{ - None = 0, - Shift = 1, - Control = 2, - Alt = 4, - Super = 8 -} diff --git a/Services/IAccessibilityService.cs b/Services/IAccessibilityService.cs index 669027d..5d22517 100644 --- a/Services/IAccessibilityService.cs +++ b/Services/IAccessibilityService.cs @@ -1,22 +1,436 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + namespace Microsoft.Maui.Platform.Linux.Services; +/// +/// Interface for accessibility services using AT-SPI2. +/// Provides screen reader support on Linux. +/// public interface IAccessibilityService { - bool IsEnabled { get; } + /// + /// Gets whether accessibility is enabled. + /// + bool IsEnabled { get; } - void Initialize(); + /// + /// Initializes the accessibility service. + /// + void Initialize(); - void Register(IAccessible accessible); + /// + /// Registers an accessible object. + /// + /// The accessible object to register. + void Register(IAccessible accessible); - void Unregister(IAccessible accessible); + /// + /// Unregisters an accessible object. + /// + /// The accessible object to unregister. + void Unregister(IAccessible accessible); - void NotifyFocusChanged(IAccessible? accessible); + /// + /// Notifies that focus has changed. + /// + /// The newly focused accessible object. + void NotifyFocusChanged(IAccessible? accessible); - void NotifyPropertyChanged(IAccessible accessible, AccessibleProperty property); + /// + /// Notifies that a property has changed. + /// + /// The accessible object. + /// The property that changed. + void NotifyPropertyChanged(IAccessible accessible, AccessibleProperty property); - void NotifyStateChanged(IAccessible accessible, AccessibleState state, bool value); + /// + /// Notifies that an accessible's state has changed. + /// + /// The accessible object. + /// The state that changed. + /// The new value of the state. + void NotifyStateChanged(IAccessible accessible, AccessibleState state, bool value); - void Announce(string text, AnnouncementPriority priority = AnnouncementPriority.Polite); + /// + /// Announces text to the screen reader. + /// + /// The text to announce. + /// The announcement priority. + void Announce(string text, AnnouncementPriority priority = AnnouncementPriority.Polite); - void Shutdown(); + /// + /// Shuts down the accessibility service. + /// + void Shutdown(); +} + +/// +/// Interface for accessible objects. +/// +public interface IAccessible +{ + /// + /// Gets the unique identifier for this accessible. + /// + string AccessibleId { get; } + + /// + /// Gets the accessible name (label for screen readers). + /// + string AccessibleName { get; } + + /// + /// Gets the accessible description (additional context). + /// + string AccessibleDescription { get; } + + /// + /// Gets the accessible role. + /// + AccessibleRole Role { get; } + + /// + /// Gets the accessible states. + /// + AccessibleStates States { get; } + + /// + /// Gets the parent accessible. + /// + IAccessible? Parent { get; } + + /// + /// Gets the child accessibles. + /// + IReadOnlyList Children { get; } + + /// + /// Gets the bounding rectangle in screen coordinates. + /// + AccessibleRect Bounds { get; } + + /// + /// Gets the available actions. + /// + IReadOnlyList Actions { get; } + + /// + /// Performs an action. + /// + /// The name of the action to perform. + /// True if the action was performed. + bool DoAction(string actionName); + + /// + /// Gets the accessible value (for sliders, progress bars, etc.). + /// + double? Value { get; } + + /// + /// Gets the minimum value. + /// + double? MinValue { get; } + + /// + /// Gets the maximum value. + /// + double? MaxValue { get; } + + /// + /// Sets the accessible value. + /// + bool SetValue(double value); +} + +/// +/// Interface for accessible text components. +/// +public interface IAccessibleText : IAccessible +{ + /// + /// Gets the text content. + /// + string Text { get; } + + /// + /// Gets the caret offset. + /// + int CaretOffset { get; } + + /// + /// Gets the number of selections. + /// + int SelectionCount { get; } + + /// + /// Gets the selection at the specified index. + /// + (int Start, int End) GetSelection(int index); + + /// + /// Sets the selection. + /// + bool SetSelection(int index, int start, int end); + + /// + /// Gets the character at the specified offset. + /// + char GetCharacterAtOffset(int offset); + + /// + /// Gets the text in the specified range. + /// + string GetTextInRange(int start, int end); + + /// + /// Gets the bounds of the character at the specified offset. + /// + AccessibleRect GetCharacterBounds(int offset); +} + +/// +/// Interface for editable text components. +/// +public interface IAccessibleEditableText : IAccessibleText +{ + /// + /// Sets the text content. + /// + bool SetText(string text); + + /// + /// Inserts text at the specified position. + /// + bool InsertText(int position, string text); + + /// + /// Deletes text in the specified range. + /// + bool DeleteText(int start, int end); + + /// + /// Copies text to clipboard. + /// + bool CopyText(int start, int end); + + /// + /// Cuts text to clipboard. + /// + bool CutText(int start, int end); + + /// + /// Pastes text from clipboard. + /// + bool PasteText(int position); +} + +/// +/// Accessible roles (based on AT-SPI2 roles). +/// +public enum AccessibleRole +{ + Unknown, + Window, + Application, + Panel, + Frame, + Button, + CheckBox, + RadioButton, + ComboBox, + Entry, + Label, + List, + ListItem, + Menu, + MenuBar, + MenuItem, + ScrollBar, + Slider, + SpinButton, + StatusBar, + Tab, + TabPanel, + Text, + ToggleButton, + ToolBar, + ToolTip, + Tree, + TreeItem, + Image, + ProgressBar, + Separator, + Link, + Table, + TableCell, + TableRow, + TableColumnHeader, + TableRowHeader, + PageTab, + PageTabList, + Dialog, + Alert, + Filler, + Icon, + Canvas +} + +/// +/// Accessible states. +/// +[Flags] +public enum AccessibleStates : long +{ + None = 0, + Active = 1L << 0, + Armed = 1L << 1, + Busy = 1L << 2, + Checked = 1L << 3, + Collapsed = 1L << 4, + Defunct = 1L << 5, + Editable = 1L << 6, + Enabled = 1L << 7, + Expandable = 1L << 8, + Expanded = 1L << 9, + Focusable = 1L << 10, + Focused = 1L << 11, + HasToolTip = 1L << 12, + Horizontal = 1L << 13, + Iconified = 1L << 14, + Modal = 1L << 15, + MultiLine = 1L << 16, + MultiSelectable = 1L << 17, + Opaque = 1L << 18, + Pressed = 1L << 19, + Resizable = 1L << 20, + Selectable = 1L << 21, + Selected = 1L << 22, + Sensitive = 1L << 23, + Showing = 1L << 24, + SingleLine = 1L << 25, + Stale = 1L << 26, + Transient = 1L << 27, + Vertical = 1L << 28, + Visible = 1L << 29, + ManagesDescendants = 1L << 30, + Indeterminate = 1L << 31, + Required = 1L << 32, + Truncated = 1L << 33, + Animated = 1L << 34, + InvalidEntry = 1L << 35, + SupportsAutocompletion = 1L << 36, + SelectableText = 1L << 37, + IsDefault = 1L << 38, + Visited = 1L << 39, + ReadOnly = 1L << 40 +} + +/// +/// Accessible state enumeration for notifications. +/// +public enum AccessibleState +{ + Active, + Armed, + Busy, + Checked, + Collapsed, + Defunct, + Editable, + Enabled, + Expandable, + Expanded, + Focusable, + Focused, + Horizontal, + Iconified, + Modal, + MultiLine, + Opaque, + Pressed, + Resizable, + Selectable, + Selected, + Sensitive, + Showing, + SingleLine, + Stale, + Transient, + Vertical, + Visible, + ManagesDescendants, + Indeterminate, + Required, + InvalidEntry, + ReadOnly +} + +/// +/// Accessible property for notifications. +/// +public enum AccessibleProperty +{ + Name, + Description, + Role, + Value, + Parent, + Children +} + +/// +/// Announcement priority. +/// +public enum AnnouncementPriority +{ + /// + /// Low priority - can be interrupted. + /// + Polite, + + /// + /// High priority - interrupts current speech. + /// + Assertive +} + +/// +/// Represents an accessible action. +/// +public class AccessibleAction +{ + /// + /// The action name. + /// + public string Name { get; set; } = string.Empty; + + /// + /// The action description. + /// + public string Description { get; set; } = string.Empty; + + /// + /// The keyboard shortcut for this action. + /// + public string? KeyBinding { get; set; } +} + +/// +/// Represents a rectangle in accessible coordinates. +/// +public struct AccessibleRect +{ + public int X { get; set; } + public int Y { get; set; } + public int Width { get; set; } + public int Height { get; set; } + + public AccessibleRect(int x, int y, int width, int height) + { + X = x; + Y = y; + Width = width; + Height = height; + } } diff --git a/Services/IAccessible.cs b/Services/IAccessible.cs deleted file mode 100644 index 6985774..0000000 --- a/Services/IAccessible.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System.Collections.Generic; - -namespace Microsoft.Maui.Platform.Linux.Services; - -public interface IAccessible -{ - string AccessibleId { get; } - - string AccessibleName { get; } - - string AccessibleDescription { get; } - - AccessibleRole Role { get; } - - AccessibleStates States { get; } - - IAccessible? Parent { get; } - - IReadOnlyList Children { get; } - - AccessibleRect Bounds { get; } - - IReadOnlyList Actions { get; } - - double? Value { get; } - - double? MinValue { get; } - - double? MaxValue { get; } - - bool DoAction(string actionName); - - bool SetValue(double value); -} diff --git a/Services/IAccessibleEditableText.cs b/Services/IAccessibleEditableText.cs deleted file mode 100644 index f9ca849..0000000 --- a/Services/IAccessibleEditableText.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace Microsoft.Maui.Platform.Linux.Services; - -public interface IAccessibleEditableText : IAccessibleText, IAccessible -{ - bool SetText(string text); - - bool InsertText(int position, string text); - - bool DeleteText(int start, int end); - - bool CopyText(int start, int end); - - bool CutText(int start, int end); - - bool PasteText(int position); -} diff --git a/Services/IAccessibleText.cs b/Services/IAccessibleText.cs deleted file mode 100644 index 2622138..0000000 --- a/Services/IAccessibleText.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace Microsoft.Maui.Platform.Linux.Services; - -public interface IAccessibleText : IAccessible -{ - string Text { get; } - - int CaretOffset { get; } - - int SelectionCount { get; } - - (int Start, int End) GetSelection(int index); - - bool SetSelection(int index, int start, int end); - - char GetCharacterAtOffset(int offset); - - string GetTextInRange(int start, int end); - - AccessibleRect GetCharacterBounds(int offset); -} diff --git a/Services/IBusInputMethodService.cs b/Services/IBusInputMethodService.cs index 7c368cd..44a594e 100644 --- a/Services/IBusInputMethodService.cs +++ b/Services/IBusInputMethodService.cs @@ -1,382 +1,379 @@ -using System; -using System.Collections.Generic; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using System.Runtime.InteropServices; +using System.Text; namespace Microsoft.Maui.Platform.Linux.Services; +/// +/// IBus Input Method service using D-Bus interface. +/// Provides modern IME support on Linux desktops. +/// public class IBusInputMethodService : IInputMethodService, IDisposable { - private delegate void IBusCommitTextCallback(IntPtr context, IntPtr text, IntPtr userData); + private nint _bus; + private nint _context; + private IInputContext? _currentContext; + private string _preEditText = string.Empty; + private int _preEditCursorPosition; + private bool _isActive; + private bool _disposed; + + // Callback delegates (prevent GC) + private IBusCommitTextCallback? _commitCallback; + private IBusUpdatePreeditTextCallback? _preeditCallback; + private IBusShowPreeditTextCallback? _showPreeditCallback; + private IBusHidePreeditTextCallback? _hidePreeditCallback; + + public bool IsActive => _isActive; + public string PreEditText => _preEditText; + public int PreEditCursorPosition => _preEditCursorPosition; + + public event EventHandler? TextCommitted; + public event EventHandler? PreEditChanged; + public event EventHandler? PreEditEnded; + + public void Initialize(nint windowHandle) + { + try + { + // Initialize IBus + ibus_init(); + + // Get the IBus bus connection + _bus = ibus_bus_new(); + if (_bus == IntPtr.Zero) + { + Console.WriteLine("IBusInputMethodService: Failed to connect to IBus"); + return; + } + + // Check if IBus is connected + if (!ibus_bus_is_connected(_bus)) + { + Console.WriteLine("IBusInputMethodService: IBus not connected"); + return; + } + + // Create input context + _context = ibus_bus_create_input_context(_bus, "maui-linux"); + if (_context == IntPtr.Zero) + { + Console.WriteLine("IBusInputMethodService: Failed to create input context"); + return; + } + + // Set capabilities + uint capabilities = IBUS_CAP_PREEDIT_TEXT | IBUS_CAP_FOCUS | IBUS_CAP_SURROUNDING_TEXT; + ibus_input_context_set_capabilities(_context, capabilities); + + // Connect signals + ConnectSignals(); + + Console.WriteLine("IBusInputMethodService: Initialized successfully"); + } + catch (Exception ex) + { + Console.WriteLine($"IBusInputMethodService: Initialization failed - {ex.Message}"); + } + } + + private void ConnectSignals() + { + if (_context == IntPtr.Zero) return; + + // Set up callbacks for IBus signals + _commitCallback = OnCommitText; + _preeditCallback = OnUpdatePreeditText; + _showPreeditCallback = OnShowPreeditText; + _hidePreeditCallback = OnHidePreeditText; + + // Connect to commit-text signal + g_signal_connect(_context, "commit-text", + Marshal.GetFunctionPointerForDelegate(_commitCallback), IntPtr.Zero); + + // Connect to update-preedit-text signal + g_signal_connect(_context, "update-preedit-text", + Marshal.GetFunctionPointerForDelegate(_preeditCallback), IntPtr.Zero); + + // Connect to show-preedit-text signal + g_signal_connect(_context, "show-preedit-text", + Marshal.GetFunctionPointerForDelegate(_showPreeditCallback), IntPtr.Zero); + + // Connect to hide-preedit-text signal + g_signal_connect(_context, "hide-preedit-text", + Marshal.GetFunctionPointerForDelegate(_hidePreeditCallback), IntPtr.Zero); + } + + private void OnCommitText(nint context, nint text, nint userData) + { + if (text == IntPtr.Zero) return; + + string committedText = GetIBusTextString(text); + if (!string.IsNullOrEmpty(committedText)) + { + _preEditText = string.Empty; + _preEditCursorPosition = 0; + _isActive = false; + + TextCommitted?.Invoke(this, new TextCommittedEventArgs(committedText)); + _currentContext?.OnTextCommitted(committedText); + } + } + + private void OnUpdatePreeditText(nint context, nint text, uint cursorPos, bool visible, nint userData) + { + if (!visible) + { + OnHidePreeditText(context, userData); + return; + } + + _isActive = true; + _preEditText = text != IntPtr.Zero ? GetIBusTextString(text) : string.Empty; + _preEditCursorPosition = (int)cursorPos; + + var attributes = GetPreeditAttributes(text); + PreEditChanged?.Invoke(this, new PreEditChangedEventArgs(_preEditText, _preEditCursorPosition, attributes)); + _currentContext?.OnPreEditChanged(_preEditText, _preEditCursorPosition); + } + + private void OnShowPreeditText(nint context, nint userData) + { + _isActive = true; + } + + private void OnHidePreeditText(nint context, nint userData) + { + _isActive = false; + _preEditText = string.Empty; + _preEditCursorPosition = 0; + + PreEditEnded?.Invoke(this, EventArgs.Empty); + _currentContext?.OnPreEditEnded(); + } + + private string GetIBusTextString(nint ibusText) + { + if (ibusText == IntPtr.Zero) return string.Empty; + + nint textPtr = ibus_text_get_text(ibusText); + if (textPtr == IntPtr.Zero) return string.Empty; + + return Marshal.PtrToStringUTF8(textPtr) ?? string.Empty; + } + + private List GetPreeditAttributes(nint ibusText) + { + var attributes = new List(); + if (ibusText == IntPtr.Zero) return attributes; + + nint attrList = ibus_text_get_attributes(ibusText); + if (attrList == IntPtr.Zero) return attributes; + + uint count = ibus_attr_list_size(attrList); + + for (uint i = 0; i < count; i++) + { + nint attr = ibus_attr_list_get(attrList, i); + if (attr == IntPtr.Zero) continue; + + var type = ibus_attribute_get_attr_type(attr); + var start = ibus_attribute_get_start_index(attr); + var end = ibus_attribute_get_end_index(attr); + + attributes.Add(new PreEditAttribute + { + Start = (int)start, + Length = (int)(end - start), + Type = ConvertAttributeType(type) + }); + } + + return attributes; + } + + private PreEditAttributeType ConvertAttributeType(uint ibusType) + { + return ibusType switch + { + IBUS_ATTR_TYPE_UNDERLINE => PreEditAttributeType.Underline, + IBUS_ATTR_TYPE_FOREGROUND => PreEditAttributeType.Highlighted, + IBUS_ATTR_TYPE_BACKGROUND => PreEditAttributeType.Reverse, + _ => PreEditAttributeType.None + }; + } + + public void SetFocus(IInputContext? context) + { + _currentContext = context; + + if (_context != IntPtr.Zero) + { + if (context != null) + { + ibus_input_context_focus_in(_context); + } + else + { + ibus_input_context_focus_out(_context); + } + } + } + + public void SetCursorLocation(int x, int y, int width, int height) + { + if (_context == IntPtr.Zero) return; + + ibus_input_context_set_cursor_location(_context, x, y, width, height); + } + + public bool ProcessKeyEvent(uint keyCode, KeyModifiers modifiers, bool isKeyDown) + { + if (_context == IntPtr.Zero) return false; + + uint state = ConvertModifiers(modifiers); + if (!isKeyDown) + { + state |= IBUS_RELEASE_MASK; + } + + return ibus_input_context_process_key_event(_context, keyCode, keyCode, state); + } + + private uint ConvertModifiers(KeyModifiers modifiers) + { + uint state = 0; + if (modifiers.HasFlag(KeyModifiers.Shift)) state |= IBUS_SHIFT_MASK; + if (modifiers.HasFlag(KeyModifiers.Control)) state |= IBUS_CONTROL_MASK; + if (modifiers.HasFlag(KeyModifiers.Alt)) state |= IBUS_MOD1_MASK; + if (modifiers.HasFlag(KeyModifiers.Super)) state |= IBUS_SUPER_MASK; + if (modifiers.HasFlag(KeyModifiers.CapsLock)) state |= IBUS_LOCK_MASK; + return state; + } + + public void Reset() + { + if (_context != IntPtr.Zero) + { + ibus_input_context_reset(_context); + } + + _preEditText = string.Empty; + _preEditCursorPosition = 0; + _isActive = false; + + PreEditEnded?.Invoke(this, EventArgs.Empty); + _currentContext?.OnPreEditEnded(); + } + + public void Shutdown() + { + Dispose(); + } + + public void Dispose() + { + if (_disposed) return; + _disposed = true; + + if (_context != IntPtr.Zero) + { + ibus_input_context_focus_out(_context); + g_object_unref(_context); + _context = IntPtr.Zero; + } + + if (_bus != IntPtr.Zero) + { + g_object_unref(_bus); + _bus = IntPtr.Zero; + } + } + + #region IBus Constants + + private const uint IBUS_CAP_PREEDIT_TEXT = 1 << 0; + private const uint IBUS_CAP_FOCUS = 1 << 3; + private const uint IBUS_CAP_SURROUNDING_TEXT = 1 << 5; + + private const uint IBUS_SHIFT_MASK = 1 << 0; + private const uint IBUS_LOCK_MASK = 1 << 1; + private const uint IBUS_CONTROL_MASK = 1 << 2; + private const uint IBUS_MOD1_MASK = 1 << 3; + private const uint IBUS_SUPER_MASK = 1 << 26; + private const uint IBUS_RELEASE_MASK = 1 << 30; + + private const uint IBUS_ATTR_TYPE_UNDERLINE = 1; + private const uint IBUS_ATTR_TYPE_FOREGROUND = 2; + private const uint IBUS_ATTR_TYPE_BACKGROUND = 3; + + #endregion + + #region IBus Interop + + private delegate void IBusCommitTextCallback(nint context, nint text, nint userData); + private delegate void IBusUpdatePreeditTextCallback(nint context, nint text, uint cursorPos, bool visible, nint userData); + private delegate void IBusShowPreeditTextCallback(nint context, nint userData); + private delegate void IBusHidePreeditTextCallback(nint context, nint userData); - private delegate void IBusUpdatePreeditTextCallback(IntPtr context, IntPtr text, uint cursorPos, bool visible, IntPtr userData); + [DllImport("libibus-1.0.so.5")] + private static extern void ibus_init(); - private delegate void IBusShowPreeditTextCallback(IntPtr context, IntPtr userData); + [DllImport("libibus-1.0.so.5")] + private static extern nint ibus_bus_new(); - private delegate void IBusHidePreeditTextCallback(IntPtr context, IntPtr userData); + [DllImport("libibus-1.0.so.5")] + private static extern bool ibus_bus_is_connected(nint bus); - private IntPtr _bus; + [DllImport("libibus-1.0.so.5")] + private static extern nint ibus_bus_create_input_context(nint bus, string clientName); - private IntPtr _context; + [DllImport("libibus-1.0.so.5")] + private static extern void ibus_input_context_set_capabilities(nint context, uint capabilities); - private IInputContext? _currentContext; + [DllImport("libibus-1.0.so.5")] + private static extern void ibus_input_context_focus_in(nint context); - private string _preEditText = string.Empty; + [DllImport("libibus-1.0.so.5")] + private static extern void ibus_input_context_focus_out(nint context); - private int _preEditCursorPosition; + [DllImport("libibus-1.0.so.5")] + private static extern void ibus_input_context_reset(nint context); - private bool _isActive; + [DllImport("libibus-1.0.so.5")] + private static extern void ibus_input_context_set_cursor_location(nint context, int x, int y, int w, int h); - private bool _disposed; + [DllImport("libibus-1.0.so.5")] + private static extern bool ibus_input_context_process_key_event(nint context, uint keyval, uint keycode, uint state); - private IBusCommitTextCallback? _commitCallback; + [DllImport("libibus-1.0.so.5")] + private static extern nint ibus_text_get_text(nint text); - private IBusUpdatePreeditTextCallback? _preeditCallback; + [DllImport("libibus-1.0.so.5")] + private static extern nint ibus_text_get_attributes(nint text); - private IBusShowPreeditTextCallback? _showPreeditCallback; + [DllImport("libibus-1.0.so.5")] + private static extern uint ibus_attr_list_size(nint attrList); - private IBusHidePreeditTextCallback? _hidePreeditCallback; + [DllImport("libibus-1.0.so.5")] + private static extern nint ibus_attr_list_get(nint attrList, uint index); - private const uint IBUS_CAP_PREEDIT_TEXT = 1u; + [DllImport("libibus-1.0.so.5")] + private static extern uint ibus_attribute_get_attr_type(nint attr); - private const uint IBUS_CAP_FOCUS = 8u; + [DllImport("libibus-1.0.so.5")] + private static extern uint ibus_attribute_get_start_index(nint attr); - private const uint IBUS_CAP_SURROUNDING_TEXT = 32u; + [DllImport("libibus-1.0.so.5")] + private static extern uint ibus_attribute_get_end_index(nint attr); - private const uint IBUS_SHIFT_MASK = 1u; + [DllImport("libgobject-2.0.so.0")] + private static extern void g_object_unref(nint obj); - private const uint IBUS_LOCK_MASK = 2u; + [DllImport("libgobject-2.0.so.0")] + private static extern ulong g_signal_connect(nint instance, string signal, nint handler, nint data); - private const uint IBUS_CONTROL_MASK = 4u; - - private const uint IBUS_MOD1_MASK = 8u; - - private const uint IBUS_SUPER_MASK = 67108864u; - - private const uint IBUS_RELEASE_MASK = 1073741824u; - - private const uint IBUS_ATTR_TYPE_UNDERLINE = 1u; - - private const uint IBUS_ATTR_TYPE_FOREGROUND = 2u; - - private const uint IBUS_ATTR_TYPE_BACKGROUND = 3u; - - public bool IsActive => _isActive; - - public string PreEditText => _preEditText; - - public int PreEditCursorPosition => _preEditCursorPosition; - - public event EventHandler? TextCommitted; - - public event EventHandler? PreEditChanged; - - public event EventHandler? PreEditEnded; - - public void Initialize(IntPtr windowHandle) - { - try - { - ibus_init(); - _bus = ibus_bus_new(); - if (_bus == IntPtr.Zero) - { - Console.WriteLine("IBusInputMethodService: Failed to connect to IBus"); - return; - } - if (!ibus_bus_is_connected(_bus)) - { - Console.WriteLine("IBusInputMethodService: IBus not connected"); - return; - } - _context = ibus_bus_create_input_context(_bus, "maui-linux"); - if (_context == IntPtr.Zero) - { - Console.WriteLine("IBusInputMethodService: Failed to create input context"); - return; - } - uint capabilities = 41u; - ibus_input_context_set_capabilities(_context, capabilities); - ConnectSignals(); - Console.WriteLine("IBusInputMethodService: Initialized successfully"); - } - catch (Exception ex) - { - Console.WriteLine("IBusInputMethodService: Initialization failed - " + ex.Message); - } - } - - private void ConnectSignals() - { - if (_context != IntPtr.Zero) - { - _commitCallback = OnCommitText; - _preeditCallback = OnUpdatePreeditText; - _showPreeditCallback = OnShowPreeditText; - _hidePreeditCallback = OnHidePreeditText; - g_signal_connect(_context, "commit-text", Marshal.GetFunctionPointerForDelegate(_commitCallback), IntPtr.Zero); - g_signal_connect(_context, "update-preedit-text", Marshal.GetFunctionPointerForDelegate(_preeditCallback), IntPtr.Zero); - g_signal_connect(_context, "show-preedit-text", Marshal.GetFunctionPointerForDelegate(_showPreeditCallback), IntPtr.Zero); - g_signal_connect(_context, "hide-preedit-text", Marshal.GetFunctionPointerForDelegate(_hidePreeditCallback), IntPtr.Zero); - } - } - - private void OnCommitText(IntPtr context, IntPtr text, IntPtr userData) - { - if (text != IntPtr.Zero) - { - string iBusTextString = GetIBusTextString(text); - if (!string.IsNullOrEmpty(iBusTextString)) - { - _preEditText = string.Empty; - _preEditCursorPosition = 0; - _isActive = false; - this.TextCommitted?.Invoke(this, new TextCommittedEventArgs(iBusTextString)); - _currentContext?.OnTextCommitted(iBusTextString); - } - } - } - - private void OnUpdatePreeditText(IntPtr context, IntPtr text, uint cursorPos, bool visible, IntPtr userData) - { - if (!visible) - { - OnHidePreeditText(context, userData); - return; - } - _isActive = true; - _preEditText = ((text != IntPtr.Zero) ? GetIBusTextString(text) : string.Empty); - _preEditCursorPosition = (int)cursorPos; - List preeditAttributes = GetPreeditAttributes(text); - this.PreEditChanged?.Invoke(this, new PreEditChangedEventArgs(_preEditText, _preEditCursorPosition, preeditAttributes)); - _currentContext?.OnPreEditChanged(_preEditText, _preEditCursorPosition); - } - - private void OnShowPreeditText(IntPtr context, IntPtr userData) - { - _isActive = true; - } - - private void OnHidePreeditText(IntPtr context, IntPtr userData) - { - _isActive = false; - _preEditText = string.Empty; - _preEditCursorPosition = 0; - this.PreEditEnded?.Invoke(this, EventArgs.Empty); - _currentContext?.OnPreEditEnded(); - } - - private string GetIBusTextString(IntPtr ibusText) - { - if (ibusText == IntPtr.Zero) - { - return string.Empty; - } - IntPtr intPtr = ibus_text_get_text(ibusText); - if (intPtr == IntPtr.Zero) - { - return string.Empty; - } - return Marshal.PtrToStringUTF8(intPtr) ?? string.Empty; - } - - private List GetPreeditAttributes(IntPtr ibusText) - { - List list = new List(); - if (ibusText == IntPtr.Zero) - { - return list; - } - IntPtr intPtr = ibus_text_get_attributes(ibusText); - if (intPtr == IntPtr.Zero) - { - return list; - } - uint num = ibus_attr_list_size(intPtr); - for (uint num2 = 0u; num2 < num; num2++) - { - IntPtr intPtr2 = ibus_attr_list_get(intPtr, num2); - if (intPtr2 != IntPtr.Zero) - { - uint ibusType = ibus_attribute_get_attr_type(intPtr2); - uint num3 = ibus_attribute_get_start_index(intPtr2); - uint num4 = ibus_attribute_get_end_index(intPtr2); - list.Add(new PreEditAttribute - { - Start = (int)num3, - Length = (int)(num4 - num3), - Type = ConvertAttributeType(ibusType) - }); - } - } - return list; - } - - private PreEditAttributeType ConvertAttributeType(uint ibusType) - { - return ibusType switch - { - 1u => PreEditAttributeType.Underline, - 2u => PreEditAttributeType.Highlighted, - 3u => PreEditAttributeType.Reverse, - _ => PreEditAttributeType.None, - }; - } - - public void SetFocus(IInputContext? context) - { - _currentContext = context; - if (_context != IntPtr.Zero) - { - if (context != null) - { - ibus_input_context_focus_in(_context); - } - else - { - ibus_input_context_focus_out(_context); - } - } - } - - public void SetCursorLocation(int x, int y, int width, int height) - { - if (_context != IntPtr.Zero) - { - ibus_input_context_set_cursor_location(_context, x, y, width, height); - } - } - - public bool ProcessKeyEvent(uint keyCode, KeyModifiers modifiers, bool isKeyDown) - { - if (_context == IntPtr.Zero) - { - return false; - } - uint num = ConvertModifiers(modifiers); - if (!isKeyDown) - { - num |= 0x40000000; - } - return ibus_input_context_process_key_event(_context, keyCode, keyCode, num); - } - - private uint ConvertModifiers(KeyModifiers modifiers) - { - uint num = 0u; - if (modifiers.HasFlag(KeyModifiers.Shift)) - { - num |= 1; - } - if (modifiers.HasFlag(KeyModifiers.Control)) - { - num |= 4; - } - if (modifiers.HasFlag(KeyModifiers.Alt)) - { - num |= 8; - } - if (modifiers.HasFlag(KeyModifiers.Super)) - { - num |= 0x4000000; - } - if (modifiers.HasFlag(KeyModifiers.CapsLock)) - { - num |= 2; - } - return num; - } - - public void Reset() - { - if (_context != IntPtr.Zero) - { - ibus_input_context_reset(_context); - } - _preEditText = string.Empty; - _preEditCursorPosition = 0; - _isActive = false; - this.PreEditEnded?.Invoke(this, EventArgs.Empty); - _currentContext?.OnPreEditEnded(); - } - - public void Shutdown() - { - Dispose(); - } - - public void Dispose() - { - if (!_disposed) - { - _disposed = true; - if (_context != IntPtr.Zero) - { - ibus_input_context_focus_out(_context); - g_object_unref(_context); - _context = IntPtr.Zero; - } - if (_bus != IntPtr.Zero) - { - g_object_unref(_bus); - _bus = IntPtr.Zero; - } - } - } - - [DllImport("libibus-1.0.so.5")] - private static extern void ibus_init(); - - [DllImport("libibus-1.0.so.5")] - private static extern IntPtr ibus_bus_new(); - - [DllImport("libibus-1.0.so.5")] - private static extern bool ibus_bus_is_connected(IntPtr bus); - - [DllImport("libibus-1.0.so.5")] - private static extern IntPtr ibus_bus_create_input_context(IntPtr bus, string clientName); - - [DllImport("libibus-1.0.so.5")] - private static extern void ibus_input_context_set_capabilities(IntPtr context, uint capabilities); - - [DllImport("libibus-1.0.so.5")] - private static extern void ibus_input_context_focus_in(IntPtr context); - - [DllImport("libibus-1.0.so.5")] - private static extern void ibus_input_context_focus_out(IntPtr context); - - [DllImport("libibus-1.0.so.5")] - private static extern void ibus_input_context_reset(IntPtr context); - - [DllImport("libibus-1.0.so.5")] - private static extern void ibus_input_context_set_cursor_location(IntPtr context, int x, int y, int w, int h); - - [DllImport("libibus-1.0.so.5")] - private static extern bool ibus_input_context_process_key_event(IntPtr context, uint keyval, uint keycode, uint state); - - [DllImport("libibus-1.0.so.5")] - private static extern IntPtr ibus_text_get_text(IntPtr text); - - [DllImport("libibus-1.0.so.5")] - private static extern IntPtr ibus_text_get_attributes(IntPtr text); - - [DllImport("libibus-1.0.so.5")] - private static extern uint ibus_attr_list_size(IntPtr attrList); - - [DllImport("libibus-1.0.so.5")] - private static extern IntPtr ibus_attr_list_get(IntPtr attrList, uint index); - - [DllImport("libibus-1.0.so.5")] - private static extern uint ibus_attribute_get_attr_type(IntPtr attr); - - [DllImport("libibus-1.0.so.5")] - private static extern uint ibus_attribute_get_start_index(IntPtr attr); - - [DllImport("libibus-1.0.so.5")] - private static extern uint ibus_attribute_get_end_index(IntPtr attr); - - [DllImport("libgobject-2.0.so.0")] - private static extern void g_object_unref(IntPtr obj); - - [DllImport("libgobject-2.0.so.0")] - private static extern ulong g_signal_connect(IntPtr instance, string signal, IntPtr handler, IntPtr data); + #endregion } diff --git a/Services/IDisplayWindow.cs b/Services/IDisplayWindow.cs deleted file mode 100644 index 24dfde2..0000000 --- a/Services/IDisplayWindow.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; - -namespace Microsoft.Maui.Platform.Linux.Services; - -public interface IDisplayWindow : IDisposable -{ - int Width { get; } - - int Height { get; } - - bool IsRunning { get; } - - event EventHandler? KeyDown; - - event EventHandler? KeyUp; - - event EventHandler? TextInput; - - event EventHandler? PointerMoved; - - event EventHandler? PointerPressed; - - event EventHandler? PointerReleased; - - event EventHandler? Scroll; - - event EventHandler? Exposed; - - event EventHandler<(int Width, int Height)>? Resized; - - event EventHandler? CloseRequested; - - void Show(); - - void Hide(); - - void SetTitle(string title); - - void Resize(int width, int height); - - void ProcessEvents(); - - void Stop(); -} diff --git a/Services/IInputContext.cs b/Services/IInputContext.cs deleted file mode 100644 index 34956d5..0000000 --- a/Services/IInputContext.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace Microsoft.Maui.Platform.Linux.Services; - -public interface IInputContext -{ - string Text { get; set; } - - int CursorPosition { get; set; } - - int SelectionStart { get; } - - int SelectionLength { get; } - - void OnTextCommitted(string text); - - void OnPreEditChanged(string preEditText, int cursorPosition); - - void OnPreEditEnded(); -} diff --git a/Services/IInputMethodService.cs b/Services/IInputMethodService.cs index 83c2cfd..8c4547b 100644 --- a/Services/IInputMethodService.cs +++ b/Services/IInputMethodService.cs @@ -1,30 +1,231 @@ -using System; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. namespace Microsoft.Maui.Platform.Linux.Services; +/// +/// Interface for Input Method Editor (IME) services. +/// Provides support for complex text input methods like CJK languages. +/// public interface IInputMethodService { - bool IsActive { get; } + /// + /// Gets whether IME is currently active. + /// + bool IsActive { get; } - string PreEditText { get; } + /// + /// Gets the current pre-edit (composition) text. + /// + string PreEditText { get; } - int PreEditCursorPosition { get; } + /// + /// Gets the cursor position within the pre-edit text. + /// + int PreEditCursorPosition { get; } - event EventHandler? TextCommitted; + /// + /// Initializes the IME service for the given window. + /// + /// The native window handle. + void Initialize(nint windowHandle); - event EventHandler? PreEditChanged; + /// + /// Sets focus to the specified input context. + /// + /// The input context to focus. + void SetFocus(IInputContext? context); - event EventHandler? PreEditEnded; + /// + /// Sets the cursor location for candidate window positioning. + /// + /// X coordinate in screen space. + /// Y coordinate in screen space. + /// Width of the cursor area. + /// Height of the cursor area. + void SetCursorLocation(int x, int y, int width, int height); - void Initialize(IntPtr windowHandle); + /// + /// Processes a key event through the IME. + /// + /// The key code. + /// Key modifiers. + /// True for key press, false for key release. + /// True if the IME handled the event. + bool ProcessKeyEvent(uint keyCode, KeyModifiers modifiers, bool isKeyDown); - void SetFocus(IInputContext? context); + /// + /// Resets the IME state, canceling any composition. + /// + void Reset(); - void SetCursorLocation(int x, int y, int width, int height); + /// + /// Shuts down the IME service. + /// + void Shutdown(); - bool ProcessKeyEvent(uint keyCode, KeyModifiers modifiers, bool isKeyDown); + /// + /// Event raised when text is committed from IME. + /// + event EventHandler? TextCommitted; - void Reset(); + /// + /// Event raised when pre-edit (composition) text changes. + /// + event EventHandler? PreEditChanged; - void Shutdown(); + /// + /// Event raised when pre-edit is completed or cancelled. + /// + event EventHandler? PreEditEnded; +} + +/// +/// Represents an input context that can receive IME input. +/// +public interface IInputContext +{ + /// + /// Gets or sets the current text content. + /// + string Text { get; set; } + + /// + /// Gets or sets the cursor position. + /// + int CursorPosition { get; set; } + + /// + /// Gets the selection start position. + /// + int SelectionStart { get; } + + /// + /// Gets the selection length. + /// + int SelectionLength { get; } + + /// + /// Called when text is committed from the IME. + /// + /// The committed text. + void OnTextCommitted(string text); + + /// + /// Called when pre-edit text changes. + /// + /// The current pre-edit text. + /// Cursor position within pre-edit text. + void OnPreEditChanged(string preEditText, int cursorPosition); + + /// + /// Called when pre-edit mode ends. + /// + void OnPreEditEnded(); +} + +/// +/// Event args for text committed events. +/// +public class TextCommittedEventArgs : EventArgs +{ + /// + /// The committed text. + /// + public string Text { get; } + + public TextCommittedEventArgs(string text) + { + Text = text; + } +} + +/// +/// Event args for pre-edit changed events. +/// +public class PreEditChangedEventArgs : EventArgs +{ + /// + /// The current pre-edit text. + /// + public string PreEditText { get; } + + /// + /// Cursor position within the pre-edit text. + /// + public int CursorPosition { get; } + + /// + /// Formatting attributes for the pre-edit text. + /// + public IReadOnlyList Attributes { get; } + + public PreEditChangedEventArgs(string preEditText, int cursorPosition, IReadOnlyList? attributes = null) + { + PreEditText = preEditText; + CursorPosition = cursorPosition; + Attributes = attributes ?? Array.Empty(); + } +} + +/// +/// Represents formatting for a portion of pre-edit text. +/// +public class PreEditAttribute +{ + /// + /// Start position in the pre-edit text. + /// + public int Start { get; set; } + + /// + /// Length of the attributed range. + /// + public int Length { get; set; } + + /// + /// The attribute type. + /// + public PreEditAttributeType Type { get; set; } +} + +/// +/// Types of pre-edit text attributes. +/// +public enum PreEditAttributeType +{ + /// + /// Normal text (no special formatting). + /// + None, + + /// + /// Underlined text (typical for composition). + /// + Underline, + + /// + /// Highlighted/selected text. + /// + Highlighted, + + /// + /// Reverse video (selected clause in some IMEs). + /// + Reverse +} + +/// +/// Key modifiers for IME processing. +/// +[Flags] +public enum KeyModifiers +{ + None = 0, + Shift = 1 << 0, + Control = 1 << 1, + Alt = 1 << 2, + Super = 1 << 3, + CapsLock = 1 << 4, + NumLock = 1 << 5 } diff --git a/Services/InputMethodServiceFactory.cs b/Services/InputMethodServiceFactory.cs index d5df8fb..612ae81 100644 --- a/Services/InputMethodServiceFactory.cs +++ b/Services/InputMethodServiceFactory.cs @@ -1,154 +1,172 @@ -using System; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using System.Runtime.InteropServices; namespace Microsoft.Maui.Platform.Linux.Services; +/// +/// Factory for creating the appropriate Input Method service. +/// Automatically selects IBus or XIM based on availability. +/// public static class InputMethodServiceFactory { - private static IInputMethodService? _instance; + private static IInputMethodService? _instance; + private static readonly object _lock = new(); - private static readonly object _lock = new object(); + /// + /// Gets the singleton input method service instance. + /// + public static IInputMethodService Instance + { + get + { + if (_instance == null) + { + lock (_lock) + { + _instance ??= CreateService(); + } + } + return _instance; + } + } - public static IInputMethodService Instance - { - get - { - if (_instance == null) - { - lock (_lock) - { - if (_instance == null) - { - _instance = CreateService(); - } - } - } - return _instance; - } - } + /// + /// Creates the most appropriate input method service for the current environment. + /// + public static IInputMethodService CreateService() + { + // Check environment variable for user preference + var imePreference = Environment.GetEnvironmentVariable("MAUI_INPUT_METHOD"); - public static IInputMethodService CreateService() - { - string environmentVariable = Environment.GetEnvironmentVariable("MAUI_INPUT_METHOD"); - if (!string.IsNullOrEmpty(environmentVariable)) - { - switch (environmentVariable.ToLowerInvariant()) - { - case "ibus": - return CreateIBusService(); - case "fcitx": - case "fcitx5": - return CreateFcitx5Service(); - case "xim": - return CreateXIMService(); - case "none": - return new NullInputMethodService(); - default: - return CreateAutoService(); - } - } - return CreateAutoService(); - } + if (!string.IsNullOrEmpty(imePreference)) + { + return imePreference.ToLowerInvariant() switch + { + "ibus" => CreateIBusService(), + "xim" => CreateXIMService(), + "none" => new NullInputMethodService(), + _ => CreateAutoService() + }; + } - private static IInputMethodService CreateAutoService() - { - string obj = Environment.GetEnvironmentVariable("GTK_IM_MODULE")?.ToLowerInvariant(); - if (obj != null && obj.Contains("fcitx") && Fcitx5InputMethodService.IsAvailable()) - { - Console.WriteLine("InputMethodServiceFactory: Using Fcitx5"); - return CreateFcitx5Service(); - } - if (IsIBusAvailable()) - { - Console.WriteLine("InputMethodServiceFactory: Using IBus"); - return CreateIBusService(); - } - if (Fcitx5InputMethodService.IsAvailable()) - { - Console.WriteLine("InputMethodServiceFactory: Using Fcitx5"); - return CreateFcitx5Service(); - } - if (IsXIMAvailable()) - { - Console.WriteLine("InputMethodServiceFactory: Using XIM"); - return CreateXIMService(); - } - Console.WriteLine("InputMethodServiceFactory: No IME available, using null service"); - return new NullInputMethodService(); - } + return CreateAutoService(); + } - private static IInputMethodService CreateIBusService() - { - try - { - return new IBusInputMethodService(); - } - catch (Exception ex) - { - Console.WriteLine("InputMethodServiceFactory: Failed to create IBus service - " + ex.Message); - return new NullInputMethodService(); - } - } + private static IInputMethodService CreateAutoService() + { + // Try IBus first (most common on modern Linux) + if (IsIBusAvailable()) + { + Console.WriteLine("InputMethodServiceFactory: Using IBus"); + return CreateIBusService(); + } - private static IInputMethodService CreateFcitx5Service() - { - try - { - return new Fcitx5InputMethodService(); - } - catch (Exception ex) - { - Console.WriteLine("InputMethodServiceFactory: Failed to create Fcitx5 service - " + ex.Message); - return new NullInputMethodService(); - } - } + // Fall back to XIM + if (IsXIMAvailable()) + { + Console.WriteLine("InputMethodServiceFactory: Using XIM"); + return CreateXIMService(); + } - private static IInputMethodService CreateXIMService() - { - try - { - return new X11InputMethodService(); - } - catch (Exception ex) - { - Console.WriteLine("InputMethodServiceFactory: Failed to create XIM service - " + ex.Message); - return new NullInputMethodService(); - } - } + // No IME available + Console.WriteLine("InputMethodServiceFactory: No IME available, using null service"); + return new NullInputMethodService(); + } - private static bool IsIBusAvailable() - { - if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("IBUS_ADDRESS"))) - { - return true; - } - try - { - NativeLibrary.Free(NativeLibrary.Load("libibus-1.0.so.5")); - return true; - } - catch - { - return false; - } - } + private static IInputMethodService CreateIBusService() + { + try + { + return new IBusInputMethodService(); + } + catch (Exception ex) + { + Console.WriteLine($"InputMethodServiceFactory: Failed to create IBus service - {ex.Message}"); + return new NullInputMethodService(); + } + } - private static bool IsXIMAvailable() - { - string environmentVariable = Environment.GetEnvironmentVariable("XMODIFIERS"); - if (!string.IsNullOrEmpty(environmentVariable) && environmentVariable.Contains("@im=")) - { - return true; - } - return !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("DISPLAY")); - } + private static IInputMethodService CreateXIMService() + { + try + { + return new X11InputMethodService(); + } + catch (Exception ex) + { + Console.WriteLine($"InputMethodServiceFactory: Failed to create XIM service - {ex.Message}"); + return new NullInputMethodService(); + } + } - public static void Reset() - { - lock (_lock) - { - _instance?.Shutdown(); - _instance = null; - } - } + private static bool IsIBusAvailable() + { + // Check if IBus daemon is running + var ibusAddress = Environment.GetEnvironmentVariable("IBUS_ADDRESS"); + if (!string.IsNullOrEmpty(ibusAddress)) + { + return true; + } + + // Try to load IBus library + try + { + var handle = NativeLibrary.Load("libibus-1.0.so.5"); + NativeLibrary.Free(handle); + return true; + } + catch + { + return false; + } + } + + private static bool IsXIMAvailable() + { + // Check XMODIFIERS environment variable + var xmodifiers = Environment.GetEnvironmentVariable("XMODIFIERS"); + if (!string.IsNullOrEmpty(xmodifiers) && xmodifiers.Contains("@im=")) + { + return true; + } + + // Check if running under X11 + var display = Environment.GetEnvironmentVariable("DISPLAY"); + return !string.IsNullOrEmpty(display); + } + + /// + /// Resets the singleton instance (useful for testing). + /// + public static void Reset() + { + lock (_lock) + { + _instance?.Shutdown(); + _instance = null; + } + } +} + +/// +/// Null implementation of IInputMethodService for when no IME is available. +/// +public class NullInputMethodService : IInputMethodService +{ + public bool IsActive => false; + public string PreEditText => string.Empty; + public int PreEditCursorPosition => 0; + + public event EventHandler? TextCommitted; + public event EventHandler? PreEditChanged; + public event EventHandler? PreEditEnded; + + public void Initialize(nint windowHandle) { } + public void SetFocus(IInputContext? context) { } + public void SetCursorLocation(int x, int y, int width, int height) { } + public bool ProcessKeyEvent(uint keyCode, KeyModifiers modifiers, bool isKeyDown) => false; + public void Reset() { } + public void Shutdown() { } } diff --git a/Services/KeyModifiers.cs b/Services/KeyModifiers.cs deleted file mode 100644 index df9edec..0000000 --- a/Services/KeyModifiers.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; - -namespace Microsoft.Maui.Platform.Linux.Services; - -[Flags] -public enum KeyModifiers -{ - None = 0, - Shift = 1, - Control = 2, - Alt = 4, - Super = 8, - CapsLock = 0x10, - NumLock = 0x20 -} diff --git a/Services/LauncherService.cs b/Services/LauncherService.cs index bfac4dd..e0c964a 100644 --- a/Services/LauncherService.cs +++ b/Services/LauncherService.cs @@ -1,77 +1,85 @@ -using System; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using System.Diagnostics; -using System.Threading.Tasks; using Microsoft.Maui.ApplicationModel; -using Microsoft.Maui.Storage; namespace Microsoft.Maui.Platform.Linux.Services; +/// +/// Linux launcher service for opening URLs and files. +/// public class LauncherService : ILauncher { - public Task CanOpenAsync(Uri uri) - { - return Task.FromResult(result: true); - } + public Task CanOpenAsync(Uri uri) + { + // On Linux, we can generally open any URI using xdg-open + return Task.FromResult(true); + } - public Task OpenAsync(Uri uri) - { - return Task.Run(delegate - { - try - { - using Process process = Process.Start(new ProcessStartInfo - { - FileName = "xdg-open", - Arguments = uri.ToString(), - UseShellExecute = false, - RedirectStandardOutput = true, - RedirectStandardError = true, - CreateNoWindow = true - }); - if (process == null) - { - return false; - } - return true; - } - catch - { - return false; - } - }); - } + public Task OpenAsync(Uri uri) + { + return Task.Run(() => + { + try + { + var psi = new ProcessStartInfo + { + FileName = "xdg-open", + Arguments = uri.ToString(), + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; - public Task OpenAsync(OpenFileRequest request) - { - if (request.File == null) - { - return Task.FromResult(result: false); - } - return Task.Run(delegate - { - try - { - string fullPath = ((FileBase)request.File).FullPath; - using Process process = Process.Start(new ProcessStartInfo - { - FileName = "xdg-open", - Arguments = "\"" + fullPath + "\"", - UseShellExecute = false, - RedirectStandardOutput = true, - RedirectStandardError = true, - CreateNoWindow = true - }); - return process != null; - } - catch - { - return false; - } - }); - } + using var process = Process.Start(psi); + if (process == null) + return false; - public Task TryOpenAsync(Uri uri) - { - return OpenAsync(uri); - } + // Don't wait for the process to exit - xdg-open may spawn another process + return true; + } + catch + { + return false; + } + }); + } + + public Task OpenAsync(OpenFileRequest request) + { + if (request.File == null) + return Task.FromResult(false); + + return Task.Run(() => + { + try + { + var filePath = request.File.FullPath; + + var psi = new ProcessStartInfo + { + FileName = "xdg-open", + Arguments = $"\"{filePath}\"", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + + using var process = Process.Start(psi); + return process != null; + } + catch + { + return false; + } + }); + } + + public Task TryOpenAsync(Uri uri) + { + return OpenAsync(uri); + } } diff --git a/Services/LinuxFileResult.cs b/Services/LinuxFileResult.cs deleted file mode 100644 index 55df44b..0000000 --- a/Services/LinuxFileResult.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Microsoft.Maui.Storage; - -namespace Microsoft.Maui.Platform.Linux.Services; - -internal class LinuxFileResult : FileResult -{ - public LinuxFileResult(string fullPath) - : base(fullPath) - { - } -} diff --git a/Services/LinuxResourcesProvider.cs b/Services/LinuxResourcesProvider.cs index 43a2e94..7b4d52e 100644 --- a/Services/LinuxResourcesProvider.cs +++ b/Services/LinuxResourcesProvider.cs @@ -1,109 +1,53 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using Microsoft.Maui.Controls; using Microsoft.Maui.Controls.Internals; +[assembly: Dependency(typeof(Microsoft.Maui.Platform.Linux.Services.LinuxResourcesProvider))] + namespace Microsoft.Maui.Platform.Linux.Services; +/// +/// Provides system resources for the Linux platform. +/// internal sealed class LinuxResourcesProvider : ISystemResourcesProvider { - private ResourceDictionary? _dictionary; + private ResourceDictionary? _dictionary; - public IResourceDictionary GetSystemResources() - { - if (_dictionary == null) - { - _dictionary = CreateResourceDictionary(); - } - return (IResourceDictionary)(object)_dictionary; - } + public IResourceDictionary GetSystemResources() + { + _dictionary ??= CreateResourceDictionary(); + return _dictionary; + } - private ResourceDictionary CreateResourceDictionary() - { - //IL_0000: Unknown result type (might be due to invalid IL or missing references) - //IL_0005: Unknown result type (might be due to invalid IL or missing references) - //IL_0015: Unknown result type (might be due to invalid IL or missing references) - //IL_001f: Expected O, but got Unknown - //IL_001f: Unknown result type (might be due to invalid IL or missing references) - //IL_002f: Unknown result type (might be due to invalid IL or missing references) - //IL_003f: Unknown result type (might be due to invalid IL or missing references) - //IL_004f: Unknown result type (might be due to invalid IL or missing references) - //IL_005f: Unknown result type (might be due to invalid IL or missing references) - //IL_0069: Expected O, but got Unknown - //IL_0069: Unknown result type (might be due to invalid IL or missing references) - //IL_007a: Expected O, but got Unknown - return new ResourceDictionary - { - [Styles.BodyStyleKey] = (object)new Style(typeof(Label)), - [Styles.TitleStyleKey] = CreateTitleStyle(), - [Styles.SubtitleStyleKey] = CreateSubtitleStyle(), - [Styles.CaptionStyleKey] = CreateCaptionStyle(), - [Styles.ListItemTextStyleKey] = (object)new Style(typeof(Label)), - [Styles.ListItemDetailTextStyleKey] = CreateCaptionStyle() - }; - } + private ResourceDictionary CreateResourceDictionary() + { + var dictionary = new ResourceDictionary(); - private static Style CreateTitleStyle() - { - //IL_000a: Unknown result type (might be due to invalid IL or missing references) - //IL_000f: Unknown result type (might be due to invalid IL or missing references) - //IL_0015: Unknown result type (might be due to invalid IL or missing references) - //IL_001a: Unknown result type (might be due to invalid IL or missing references) - //IL_0025: Unknown result type (might be due to invalid IL or missing references) - //IL_003e: Expected O, but got Unknown - //IL_003f: Expected O, but got Unknown - return new Style(typeof(Label)) - { - Setters = - { - new Setter - { - Property = Label.FontSizeProperty, - Value = 24.0 - } - } - }; - } + // Add default styles + dictionary[Device.Styles.BodyStyleKey] = new Style(typeof(Label)); + dictionary[Device.Styles.TitleStyleKey] = CreateTitleStyle(); + dictionary[Device.Styles.SubtitleStyleKey] = CreateSubtitleStyle(); + dictionary[Device.Styles.CaptionStyleKey] = CreateCaptionStyle(); + dictionary[Device.Styles.ListItemTextStyleKey] = new Style(typeof(Label)); + dictionary[Device.Styles.ListItemDetailTextStyleKey] = CreateCaptionStyle(); - private static Style CreateSubtitleStyle() - { - //IL_000a: Unknown result type (might be due to invalid IL or missing references) - //IL_000f: Unknown result type (might be due to invalid IL or missing references) - //IL_0015: Unknown result type (might be due to invalid IL or missing references) - //IL_001a: Unknown result type (might be due to invalid IL or missing references) - //IL_0025: Unknown result type (might be due to invalid IL or missing references) - //IL_003e: Expected O, but got Unknown - //IL_003f: Expected O, but got Unknown - return new Style(typeof(Label)) - { - Setters = - { - new Setter - { - Property = Label.FontSizeProperty, - Value = 18.0 - } - } - }; - } + return dictionary; + } - private static Style CreateCaptionStyle() - { - //IL_000a: Unknown result type (might be due to invalid IL or missing references) - //IL_000f: Unknown result type (might be due to invalid IL or missing references) - //IL_0015: Unknown result type (might be due to invalid IL or missing references) - //IL_001a: Unknown result type (might be due to invalid IL or missing references) - //IL_0025: Unknown result type (might be due to invalid IL or missing references) - //IL_003e: Expected O, but got Unknown - //IL_003f: Expected O, but got Unknown - return new Style(typeof(Label)) - { - Setters = - { - new Setter - { - Property = Label.FontSizeProperty, - Value = 12.0 - } - } - }; - } + private static Style CreateTitleStyle() => new(typeof(Label)) + { + Setters = { new Setter { Property = Label.FontSizeProperty, Value = 24.0 } } + }; + + private static Style CreateSubtitleStyle() => new(typeof(Label)) + { + Setters = { new Setter { Property = Label.FontSizeProperty, Value = 18.0 } } + }; + + private static Style CreateCaptionStyle() => new(typeof(Label)) + { + Setters = { new Setter { Property = Label.FontSizeProperty, Value = 12.0 } } + }; } diff --git a/Services/MauiIconGenerator.cs b/Services/MauiIconGenerator.cs deleted file mode 100644 index e0012fa..0000000 --- a/Services/MauiIconGenerator.cs +++ /dev/null @@ -1,184 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using SkiaSharp; -using Svg.Skia; - -namespace Microsoft.Maui.Platform.Linux.Services; - -public static class MauiIconGenerator -{ - private const int DefaultIconSize = 256; - - public static string? GenerateIcon(string metaFilePath) - { - //IL_0095: Unknown result type (might be due to invalid IL or missing references) - //IL_008c: Unknown result type (might be due to invalid IL or missing references) - //IL_009a: Unknown result type (might be due to invalid IL or missing references) - //IL_0123: Unknown result type (might be due to invalid IL or missing references) - //IL_0165: Unknown result type (might be due to invalid IL or missing references) - //IL_017c: Unknown result type (might be due to invalid IL or missing references) - //IL_018e: Unknown result type (might be due to invalid IL or missing references) - //IL_0195: Expected O, but got Unknown - //IL_01b5: Unknown result type (might be due to invalid IL or missing references) - //IL_01ba: Unknown result type (might be due to invalid IL or missing references) - if (!File.Exists(metaFilePath)) - { - Console.WriteLine("[MauiIconGenerator] Metadata file not found: " + metaFilePath); - return null; - } - try - { - string? path = Path.GetDirectoryName(metaFilePath) ?? ""; - Dictionary dictionary = ParseMetadata(File.ReadAllText(metaFilePath)); - Path.Combine(path, "appicon_bg.svg"); - string text = Path.Combine(path, "appicon_fg.svg"); - string text2 = Path.Combine(path, "appicon.png"); - string value; - int result; - int num = ((dictionary.TryGetValue("Size", out value) && int.TryParse(value, out result)) ? result : 256); - string value2; - SKColor val = (dictionary.TryGetValue("Color", out value2) ? ParseColor(value2) : SKColors.Purple); - string value3; - float result2; - float num2 = ((dictionary.TryGetValue("Scale", out value3) && float.TryParse(value3, out result2)) ? result2 : 0.65f); - Console.WriteLine($"[MauiIconGenerator] Generating {num}x{num} icon"); - Console.WriteLine($"[MauiIconGenerator] Color: {val}"); - Console.WriteLine($"[MauiIconGenerator] Scale: {num2}"); - SKSurface val2 = SKSurface.Create(new SKImageInfo(num, num, (SKColorType)4, (SKAlphaType)2)); - try - { - SKCanvas canvas = val2.Canvas; - canvas.Clear(val); - if (File.Exists(text)) - { - SKSvg val3 = new SKSvg(); - try - { - if (val3.Load(text) != null && val3.Picture != null) - { - SKRect cullRect = val3.Picture.CullRect; - float num3 = (float)num * num2 / Math.Max(((SKRect)(ref cullRect)).Width, ((SKRect)(ref cullRect)).Height); - float num4 = ((float)num - ((SKRect)(ref cullRect)).Width * num3) / 2f; - float num5 = ((float)num - ((SKRect)(ref cullRect)).Height * num3) / 2f; - canvas.Save(); - canvas.Translate(num4, num5); - canvas.Scale(num3); - canvas.DrawPicture(val3.Picture, (SKPaint)null); - canvas.Restore(); - } - } - finally - { - ((IDisposable)val3)?.Dispose(); - } - } - SKImage val4 = val2.Snapshot(); - try - { - SKData val5 = val4.Encode((SKEncodedImageFormat)4, 100); - try - { - using FileStream fileStream = File.OpenWrite(text2); - val5.SaveTo((Stream)fileStream); - Console.WriteLine("[MauiIconGenerator] Generated: " + text2); - return text2; - } - finally - { - ((IDisposable)val5)?.Dispose(); - } - } - finally - { - ((IDisposable)val4)?.Dispose(); - } - } - finally - { - ((IDisposable)val2)?.Dispose(); - } - } - catch (Exception ex) - { - Console.WriteLine("[MauiIconGenerator] Error: " + ex.Message); - return null; - } - } - - private static Dictionary ParseMetadata(string content) - { - Dictionary dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); - string[] array = content.Split(new char[2] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); - for (int i = 0; i < array.Length; i++) - { - string[] array2 = array[i].Split('=', 2); - if (array2.Length == 2) - { - dictionary[array2[0].Trim()] = array2[1].Trim(); - } - } - return dictionary; - } - - private static SKColor ParseColor(string colorStr) - { - //IL_0008: Unknown result type (might be due to invalid IL or missing references) - //IL_0246: Unknown result type (might be due to invalid IL or missing references) - //IL_024b: Unknown result type (might be due to invalid IL or missing references) - //IL_024d: Unknown result type (might be due to invalid IL or missing references) - //IL_0207: Unknown result type (might be due to invalid IL or missing references) - //IL_020c: Unknown result type (might be due to invalid IL or missing references) - //IL_0219: Unknown result type (might be due to invalid IL or missing references) - //IL_021e: Unknown result type (might be due to invalid IL or missing references) - //IL_00d2: Unknown result type (might be due to invalid IL or missing references) - //IL_023d: Unknown result type (might be due to invalid IL or missing references) - //IL_0242: Unknown result type (might be due to invalid IL or missing references) - //IL_022b: Unknown result type (might be due to invalid IL or missing references) - //IL_0230: Unknown result type (might be due to invalid IL or missing references) - //IL_0119: Unknown result type (might be due to invalid IL or missing references) - //IL_0210: Unknown result type (might be due to invalid IL or missing references) - //IL_0215: Unknown result type (might be due to invalid IL or missing references) - //IL_0222: Unknown result type (might be due to invalid IL or missing references) - //IL_0227: Unknown result type (might be due to invalid IL or missing references) - //IL_0234: Unknown result type (might be due to invalid IL or missing references) - //IL_0239: Unknown result type (might be due to invalid IL or missing references) - if (string.IsNullOrEmpty(colorStr)) - { - return SKColors.Purple; - } - colorStr = colorStr.Trim(); - if (colorStr.StartsWith("#")) - { - string text = colorStr.Substring(1); - if (text.Length == 3) - { - text = $"{text[0]}{text[0]}{text[1]}{text[1]}{text[2]}{text[2]}"; - } - uint result2; - if (text.Length == 6) - { - if (uint.TryParse(text, NumberStyles.HexNumber, null, out var result)) - { - return new SKColor((byte)((result >> 16) & 0xFF), (byte)((result >> 8) & 0xFF), (byte)(result & 0xFF)); - } - } - else if (text.Length == 8 && uint.TryParse(text, NumberStyles.HexNumber, null, out result2)) - { - return new SKColor((byte)((result2 >> 16) & 0xFF), (byte)((result2 >> 8) & 0xFF), (byte)(result2 & 0xFF), (byte)((result2 >> 24) & 0xFF)); - } - } - return (SKColor)(colorStr.ToLowerInvariant() switch - { - "red" => SKColors.Red, - "green" => SKColors.Green, - "blue" => SKColors.Blue, - "purple" => SKColors.Purple, - "orange" => SKColors.Orange, - "white" => SKColors.White, - "black" => SKColors.Black, - _ => SKColors.Purple, - }); - } -} diff --git a/Services/NotificationAction.cs b/Services/NotificationAction.cs deleted file mode 100644 index fc91fd1..0000000 --- a/Services/NotificationAction.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; - -namespace Microsoft.Maui.Platform.Linux.Services; - -public class NotificationAction -{ - public string Key { get; set; } = ""; - - public string Label { get; set; } = ""; - - public Action? Callback { get; set; } - - public NotificationAction() - { - } - - public NotificationAction(string key, string label, Action? callback = null) - { - Key = key; - Label = label; - Callback = callback; - } -} diff --git a/Services/NotificationActionEventArgs.cs b/Services/NotificationActionEventArgs.cs deleted file mode 100644 index 584e07f..0000000 --- a/Services/NotificationActionEventArgs.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; - -namespace Microsoft.Maui.Platform.Linux.Services; - -public class NotificationActionEventArgs : EventArgs -{ - public uint NotificationId { get; } - - public string ActionKey { get; } - - public string? Tag { get; } - - public NotificationActionEventArgs(uint notificationId, string actionKey, string? tag) - { - NotificationId = notificationId; - ActionKey = actionKey; - Tag = tag; - } -} diff --git a/Services/NotificationCloseReason.cs b/Services/NotificationCloseReason.cs deleted file mode 100644 index 3792400..0000000 --- a/Services/NotificationCloseReason.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Microsoft.Maui.Platform.Linux.Services; - -public enum NotificationCloseReason -{ - Expired = 1, - Dismissed, - Closed, - Undefined -} diff --git a/Services/NotificationClosedEventArgs.cs b/Services/NotificationClosedEventArgs.cs deleted file mode 100644 index bfb5c07..0000000 --- a/Services/NotificationClosedEventArgs.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; - -namespace Microsoft.Maui.Platform.Linux.Services; - -public class NotificationClosedEventArgs : EventArgs -{ - public uint NotificationId { get; } - - public NotificationCloseReason Reason { get; } - - public string? Tag { get; } - - public NotificationClosedEventArgs(uint notificationId, NotificationCloseReason reason, string? tag) - { - NotificationId = notificationId; - Reason = reason; - Tag = tag; - } -} diff --git a/Services/NotificationContext.cs b/Services/NotificationContext.cs deleted file mode 100644 index f462498..0000000 --- a/Services/NotificationContext.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Microsoft.Maui.Platform.Linux.Services; - -internal class NotificationContext -{ - public string? Tag { get; set; } - - public Dictionary? ActionCallbacks { get; set; } -} diff --git a/Services/NotificationOptions.cs b/Services/NotificationOptions.cs deleted file mode 100644 index 51764dc..0000000 --- a/Services/NotificationOptions.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Collections.Generic; - -namespace Microsoft.Maui.Platform.Linux.Services; - -public class NotificationOptions -{ - public string Title { get; set; } = ""; - - public string Message { get; set; } = ""; - - public string? IconPath { get; set; } - - public string? IconName { get; set; } - - public NotificationUrgency Urgency { get; set; } = NotificationUrgency.Normal; - - public int ExpireTimeMs { get; set; } = 5000; - - public string? Category { get; set; } - - public bool IsTransient { get; set; } - - public Dictionary? Actions { get; set; } -} diff --git a/Services/NotificationService.cs b/Services/NotificationService.cs index 164e131..da52861 100644 --- a/Services/NotificationService.cs +++ b/Services/NotificationService.cs @@ -1,370 +1,211 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading.Tasks; namespace Microsoft.Maui.Platform.Linux.Services; +/// +/// Linux notification service using notify-send (libnotify). +/// public class NotificationService { - private readonly string _appName; + private readonly string _appName; + private readonly string? _defaultIconPath; - private readonly string? _defaultIconPath; + public NotificationService(string appName = "MAUI Application", string? defaultIconPath = null) + { + _appName = appName; + _defaultIconPath = defaultIconPath; + } - private readonly ConcurrentDictionary _activeNotifications = new ConcurrentDictionary(); + /// + /// Shows a simple notification. + /// + public async Task ShowAsync(string title, string message) + { + await ShowAsync(new NotificationOptions + { + Title = title, + Message = message + }); + } - private static uint _notificationIdCounter = 1u; + /// + /// Shows a notification with options. + /// + public async Task ShowAsync(NotificationOptions options) + { + try + { + var args = BuildNotifyArgs(options); - private Process? _dBusMonitor; + var startInfo = new ProcessStartInfo + { + FileName = "notify-send", + Arguments = args, + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; - private bool _monitoringActions; + using var process = Process.Start(startInfo); + if (process != null) + { + await process.WaitForExitAsync(); + } + } + catch (Exception ex) + { + // Fall back to zenity notification + await TryZenityNotification(options); + } + } - public event EventHandler? ActionInvoked; + private string BuildNotifyArgs(NotificationOptions options) + { + var args = new List(); - public event EventHandler? NotificationClosed; + // App name + args.Add($"--app-name=\"{EscapeArg(_appName)}\""); - public NotificationService(string appName = "MAUI Application", string? defaultIconPath = null) - { - _appName = appName; - _defaultIconPath = defaultIconPath; - } + // Urgency + args.Add($"--urgency={options.Urgency.ToString().ToLower()}"); - public void StartActionMonitoring() - { - if (!_monitoringActions) - { - _monitoringActions = true; - Task.Run((Func)MonitorNotificationSignals); - } - } + // Expire time (milliseconds, 0 = never expire) + if (options.ExpireTimeMs > 0) + { + args.Add($"--expire-time={options.ExpireTimeMs}"); + } - public void StopActionMonitoring() - { - _monitoringActions = false; - try - { - _dBusMonitor?.Kill(); - _dBusMonitor?.Dispose(); - _dBusMonitor = null; - } - catch - { - } - } + // Icon + var icon = options.IconPath ?? _defaultIconPath; + if (!string.IsNullOrEmpty(icon)) + { + args.Add($"--icon=\"{EscapeArg(icon)}\""); + } + else if (!string.IsNullOrEmpty(options.IconName)) + { + args.Add($"--icon={options.IconName}"); + } - private async Task MonitorNotificationSignals() - { - _ = 2; - try - { - ProcessStartInfo startInfo = new ProcessStartInfo - { - FileName = "dbus-monitor", - Arguments = "--session \"interface='org.freedesktop.Notifications'\"", - UseShellExecute = false, - RedirectStandardOutput = true, - RedirectStandardError = true, - CreateNoWindow = true - }; - _dBusMonitor = Process.Start(startInfo); - if (_dBusMonitor == null) - { - return; - } - StreamReader reader = _dBusMonitor.StandardOutput; - StringBuilder buffer = new StringBuilder(); - while (_monitoringActions && !_dBusMonitor.HasExited) - { - string text = await reader.ReadLineAsync(); - if (text != null) - { - buffer.AppendLine(text); - if (text.Contains("ActionInvoked")) - { - await ProcessActionInvoked(reader); - } - else if (text.Contains("NotificationClosed")) - { - await ProcessNotificationClosed(reader); - } - continue; - } - break; - } - } - catch (Exception ex) - { - Console.WriteLine("[NotificationService] D-Bus monitor error: " + ex.Message); - } - } + // Category + if (!string.IsNullOrEmpty(options.Category)) + { + args.Add($"--category={options.Category}"); + } - private async Task ProcessActionInvoked(StreamReader reader) - { - try - { - uint notificationId = 0u; - string actionKey = null; - for (int i = 0; i < 10; i++) - { - string text = await reader.ReadLineAsync(); - if (text == null) - { - break; - } - if (text.Contains("uint32")) - { - Match match = Regex.Match(text, "uint32\\s+(\\d+)"); - if (match.Success) - { - notificationId = uint.Parse(match.Groups[1].Value); - } - } - else if (text.Contains("string")) - { - Match match2 = Regex.Match(text, "string\\s+\"([^\"]*)\""); - if (match2.Success && actionKey == null) - { - actionKey = match2.Groups[1].Value; - } - } - if (notificationId != 0 && actionKey != null) - { - break; - } - } - if (notificationId != 0 && actionKey != null && _activeNotifications.TryGetValue(notificationId, out NotificationContext value)) - { - Action value2 = default(Action); - if (value.ActionCallbacks?.TryGetValue(actionKey, out value2) ?? false) - { - value2?.Invoke(); - } - this.ActionInvoked?.Invoke(this, new NotificationActionEventArgs(notificationId, actionKey, value.Tag)); - } - } - catch - { - } - } + // Hint for transient notifications + if (options.IsTransient) + { + args.Add("--hint=int:transient:1"); + } - private async Task ProcessNotificationClosed(StreamReader reader) - { - try - { - uint notificationId = 0u; - uint reason = 0u; - for (int i = 0; i < 5; i++) - { - string text = await reader.ReadLineAsync(); - if (text == null) - { - break; - } - if (!text.Contains("uint32")) - { - continue; - } - Match match = Regex.Match(text, "uint32\\s+(\\d+)"); - if (match.Success) - { - if (notificationId == 0) - { - notificationId = uint.Parse(match.Groups[1].Value); - } - else - { - reason = uint.Parse(match.Groups[1].Value); - } - } - } - if (notificationId != 0) - { - _activeNotifications.TryRemove(notificationId, out NotificationContext value); - this.NotificationClosed?.Invoke(this, new NotificationClosedEventArgs(notificationId, (NotificationCloseReason)reason, value?.Tag)); - } - } - catch - { - } - } + // Actions (if supported) + if (options.Actions?.Count > 0) + { + foreach (var action in options.Actions) + { + args.Add($"--action=\"{action.Key}={EscapeArg(action.Value)}\""); + } + } - public async Task ShowAsync(string title, string message) - { - await ShowAsync(new NotificationOptions - { - Title = title, - Message = message - }); - } + // Title and message + args.Add($"\"{EscapeArg(options.Title)}\""); + args.Add($"\"{EscapeArg(options.Message)}\""); - public async Task ShowWithActionsAsync(string title, string message, IEnumerable actions, string? tag = null) - { - uint notificationId = _notificationIdCounter++; - NotificationContext value = new NotificationContext - { - Tag = tag, - ActionCallbacks = actions.ToDictionary((NotificationAction a) => a.Key, (NotificationAction a) => a.Callback) - }; - _activeNotifications[notificationId] = value; - Dictionary actions2 = actions.ToDictionary((NotificationAction a) => a.Key, (NotificationAction a) => a.Label); - await ShowAsync(new NotificationOptions - { - Title = title, - Message = message, - Actions = actions2 - }); - return notificationId; - } + return string.Join(" ", args); + } - public async Task CancelAsync(uint notificationId) - { - try - { - ProcessStartInfo startInfo = new ProcessStartInfo - { - FileName = "gdbus", - Arguments = $"call --session --dest org.freedesktop.Notifications --object-path /org/freedesktop/Notifications --method org.freedesktop.Notifications.CloseNotification {notificationId}", - UseShellExecute = false, - CreateNoWindow = true - }; - using Process process = Process.Start(startInfo); - if (process != null) - { - await process.WaitForExitAsync(); - } - _activeNotifications.TryRemove(notificationId, out NotificationContext _); - } - catch - { - } - } + private async Task TryZenityNotification(NotificationOptions options) + { + try + { + var iconArg = ""; + if (!string.IsNullOrEmpty(options.IconPath)) + { + iconArg = $"--window-icon=\"{options.IconPath}\""; + } - public async Task ShowAsync(NotificationOptions options) - { - try - { - string arguments = BuildNotifyArgs(options); - ProcessStartInfo startInfo = new ProcessStartInfo - { - FileName = "notify-send", - Arguments = arguments, - UseShellExecute = false, - RedirectStandardOutput = true, - RedirectStandardError = true, - CreateNoWindow = true - }; - using Process process = Process.Start(startInfo); - if (process != null) - { - await process.WaitForExitAsync(); - } - } - catch (Exception ex) - { - _ = ex; - await TryZenityNotification(options); - } - } + var typeArg = options.Urgency == NotificationUrgency.Critical ? "--error" : "--info"; - private string BuildNotifyArgs(NotificationOptions options) - { - List list = new List(); - list.Add("--app-name=\"" + EscapeArg(_appName) + "\""); - list.Add("--urgency=" + options.Urgency.ToString().ToLower()); - if (options.ExpireTimeMs > 0) - { - list.Add($"--expire-time={options.ExpireTimeMs}"); - } - string text = options.IconPath ?? _defaultIconPath; - if (!string.IsNullOrEmpty(text)) - { - list.Add("--icon=\"" + EscapeArg(text) + "\""); - } - else if (!string.IsNullOrEmpty(options.IconName)) - { - list.Add("--icon=" + options.IconName); - } - if (!string.IsNullOrEmpty(options.Category)) - { - list.Add("--category=" + options.Category); - } - if (options.IsTransient) - { - list.Add("--hint=int:transient:1"); - } - Dictionary? actions = options.Actions; - if (actions != null && actions.Count > 0) - { - foreach (KeyValuePair action in options.Actions) - { - list.Add($"--action=\"{action.Key}={EscapeArg(action.Value)}\""); - } - } - list.Add("\"" + EscapeArg(options.Title) + "\""); - list.Add("\"" + EscapeArg(options.Message) + "\""); - return string.Join(" ", list); - } + var startInfo = new ProcessStartInfo + { + FileName = "zenity", + Arguments = $"{typeArg} {iconArg} --title=\"{EscapeArg(options.Title)}\" --text=\"{EscapeArg(options.Message)}\" --timeout=5", + UseShellExecute = false, + CreateNoWindow = true + }; - private async Task TryZenityNotification(NotificationOptions options) - { - try - { - string value = ""; - if (!string.IsNullOrEmpty(options.IconPath)) - { - value = "--window-icon=\"" + options.IconPath + "\""; - } - string value2 = ((options.Urgency == NotificationUrgency.Critical) ? "--error" : "--info"); - ProcessStartInfo startInfo = new ProcessStartInfo - { - FileName = "zenity", - Arguments = $"{value2} {value} --title=\"{EscapeArg(options.Title)}\" --text=\"{EscapeArg(options.Message)}\" --timeout=5", - UseShellExecute = false, - CreateNoWindow = true - }; - using Process process = Process.Start(startInfo); - if (process != null) - { - await process.WaitForExitAsync(); - } - } - catch - { - } - } + using var process = Process.Start(startInfo); + if (process != null) + { + await process.WaitForExitAsync(); + } + } + catch + { + // Silently fail if no notification method available + } + } - public static bool IsAvailable() - { - try - { - using Process process = Process.Start(new ProcessStartInfo - { - FileName = "which", - Arguments = "notify-send", - UseShellExecute = false, - RedirectStandardOutput = true, - CreateNoWindow = true - }); - if (process == null) - { - return false; - } - process.WaitForExit(); - return process.ExitCode == 0; - } - catch - { - return false; - } - } + /// + /// Checks if notifications are available on this system. + /// + public static bool IsAvailable() + { + try + { + var startInfo = new ProcessStartInfo + { + FileName = "which", + Arguments = "notify-send", + UseShellExecute = false, + RedirectStandardOutput = true, + CreateNoWindow = true + }; - private static string EscapeArg(string arg) - { - return arg?.Replace("\\", "\\\\").Replace("\"", "\\\"") ?? ""; - } + using var process = Process.Start(startInfo); + if (process == null) return false; + + process.WaitForExit(); + return process.ExitCode == 0; + } + catch + { + return false; + } + } + + private static string EscapeArg(string arg) + { + return arg?.Replace("\\", "\\\\").Replace("\"", "\\\"") ?? ""; + } +} + +/// +/// Options for displaying a notification. +/// +public class NotificationOptions +{ + public string Title { get; set; } = ""; + public string Message { get; set; } = ""; + public string? IconPath { get; set; } + public string? IconName { get; set; } // Standard icon name like "dialog-information" + public NotificationUrgency Urgency { get; set; } = NotificationUrgency.Normal; + public int ExpireTimeMs { get; set; } = 5000; // 5 seconds default + public string? Category { get; set; } // e.g., "email", "im", "transfer" + public bool IsTransient { get; set; } + public Dictionary? Actions { get; set; } +} + +/// +/// Notification urgency level. +/// +public enum NotificationUrgency +{ + Low, + Normal, + Critical } diff --git a/Services/NotificationUrgency.cs b/Services/NotificationUrgency.cs deleted file mode 100644 index 47b5e01..0000000 --- a/Services/NotificationUrgency.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Microsoft.Maui.Platform.Linux.Services; - -public enum NotificationUrgency -{ - Low, - Normal, - Critical -} diff --git a/Services/NullAccessibilityService.cs b/Services/NullAccessibilityService.cs deleted file mode 100644 index 153aeb7..0000000 --- a/Services/NullAccessibilityService.cs +++ /dev/null @@ -1,38 +0,0 @@ -namespace Microsoft.Maui.Platform.Linux.Services; - -public class NullAccessibilityService : IAccessibilityService -{ - public bool IsEnabled => false; - - public void Initialize() - { - } - - public void Register(IAccessible accessible) - { - } - - public void Unregister(IAccessible accessible) - { - } - - public void NotifyFocusChanged(IAccessible? accessible) - { - } - - public void NotifyPropertyChanged(IAccessible accessible, AccessibleProperty property) - { - } - - public void NotifyStateChanged(IAccessible accessible, AccessibleState state, bool value) - { - } - - public void Announce(string text, AnnouncementPriority priority = AnnouncementPriority.Polite) - { - } - - public void Shutdown() - { - } -} diff --git a/Services/NullInputMethodService.cs b/Services/NullInputMethodService.cs deleted file mode 100644 index 647a807..0000000 --- a/Services/NullInputMethodService.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; - -namespace Microsoft.Maui.Platform.Linux.Services; - -public class NullInputMethodService : IInputMethodService -{ - public bool IsActive => false; - - public string PreEditText => string.Empty; - - public int PreEditCursorPosition => 0; - - public event EventHandler? TextCommitted; - - public event EventHandler? PreEditChanged; - - public event EventHandler? PreEditEnded; - - public void Initialize(IntPtr windowHandle) - { - } - - public void SetFocus(IInputContext? context) - { - } - - public void SetCursorLocation(int x, int y, int width, int height) - { - } - - public bool ProcessKeyEvent(uint keyCode, KeyModifiers modifiers, bool isKeyDown) - { - return false; - } - - public void Reset() - { - } - - public void Shutdown() - { - } -} diff --git a/Services/PortalFilePickerService.cs b/Services/PortalFilePickerService.cs deleted file mode 100644 index 84531a5..0000000 --- a/Services/PortalFilePickerService.cs +++ /dev/null @@ -1,384 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Microsoft.Maui.Storage; - -namespace Microsoft.Maui.Platform.Linux.Services; - -public class PortalFilePickerService : IFilePicker -{ - private bool _portalAvailable = true; - - private string? _fallbackTool; - - public PortalFilePickerService() - { - DetectAvailableTools(); - } - - private void DetectAvailableTools() - { - _portalAvailable = CheckPortalAvailable(); - if (!_portalAvailable) - { - if (IsCommandAvailable("zenity")) - { - _fallbackTool = "zenity"; - } - else if (IsCommandAvailable("kdialog")) - { - _fallbackTool = "kdialog"; - } - else if (IsCommandAvailable("yad")) - { - _fallbackTool = "yad"; - } - } - } - - private bool CheckPortalAvailable() - { - try - { - return RunCommand("busctl", "--user list | grep -q org.freedesktop.portal.Desktop && echo yes").Trim() == "yes"; - } - catch - { - return false; - } - } - - private bool IsCommandAvailable(string command) - { - try - { - return !string.IsNullOrWhiteSpace(RunCommand("which", command)); - } - catch - { - return false; - } - } - - public async Task PickAsync(PickOptions? options = null) - { - if (options == null) - { - options = new PickOptions(); - } - return (await PickFilesAsync(options, allowMultiple: false)).FirstOrDefault(); - } - - public async Task> PickMultipleAsync(PickOptions? options = null) - { - if (options == null) - { - options = new PickOptions(); - } - return await PickFilesAsync(options, allowMultiple: true); - } - - private async Task> PickFilesAsync(PickOptions options, bool allowMultiple) - { - if (_portalAvailable) - { - return await PickWithPortalAsync(options, allowMultiple); - } - if (_fallbackTool != null) - { - return await PickWithFallbackAsync(options, allowMultiple); - } - Console.WriteLine("[FilePickerService] No file picker available (install xdg-desktop-portal, zenity, or kdialog)"); - return Enumerable.Empty(); - } - - private async Task> PickWithPortalAsync(PickOptions options, bool allowMultiple) - { - IEnumerable result = default(IEnumerable); - object obj; - int num; - try - { - string text = BuildPortalFilterArgs(options.FileTypes); - string value = (allowMultiple ? "true" : "false"); - string input = options.PickerTitle ?? "Open File"; - StringBuilder args = new StringBuilder(); - args.Append("call --session "); - args.Append("--dest org.freedesktop.portal.Desktop "); - args.Append("--object-path /org/freedesktop/portal/desktop "); - args.Append("--method org.freedesktop.portal.FileChooser.OpenFile "); - args.Append("\"\" "); - StringBuilder stringBuilder = args; - StringBuilder stringBuilder2 = stringBuilder; - StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(3, 1, stringBuilder); - handler.AppendLiteral("\""); - handler.AppendFormatted(EscapeForShell(input)); - handler.AppendLiteral("\" "); - stringBuilder2.Append(ref handler); - args.Append("@a{sv} {"); - stringBuilder = args; - StringBuilder stringBuilder3 = stringBuilder; - handler = new StringBuilder.AppendInterpolatedStringHandler(14, 1, stringBuilder); - handler.AppendLiteral("'multiple': <"); - handler.AppendFormatted(value); - handler.AppendLiteral(">"); - stringBuilder3.Append(ref handler); - if (text != null) - { - stringBuilder = args; - StringBuilder stringBuilder4 = stringBuilder; - handler = new StringBuilder.AppendInterpolatedStringHandler(15, 1, stringBuilder); - handler.AppendLiteral(", 'filters': <"); - handler.AppendFormatted(text); - handler.AppendLiteral(">"); - stringBuilder4.Append(ref handler); - } - args.Append("}"); - if (string.IsNullOrEmpty(ParseRequestPath(await Task.Run(() => RunCommand("gdbus", args.ToString()))))) - { - result = Enumerable.Empty(); - return result; - } - await Task.Delay(100); - if (_fallbackTool != null) - { - result = await PickWithFallbackAsync(options, allowMultiple); - return result; - } - result = Enumerable.Empty(); - return result; - } - catch (Exception ex) - { - obj = ex; - num = 1; - } - if (num != 1) - { - return result; - } - Exception ex2 = (Exception)obj; - Console.WriteLine("[FilePickerService] Portal error: " + ex2.Message); - if (_fallbackTool != null) - { - return await PickWithFallbackAsync(options, allowMultiple); - } - return Enumerable.Empty(); - } - - private async Task> PickWithFallbackAsync(PickOptions options, bool allowMultiple) - { - return _fallbackTool switch - { - "zenity" => await PickWithZenityAsync(options, allowMultiple), - "kdialog" => await PickWithKdialogAsync(options, allowMultiple), - "yad" => await PickWithYadAsync(options, allowMultiple), - _ => Enumerable.Empty(), - }; - } - - private async Task> PickWithZenityAsync(PickOptions options, bool allowMultiple) - { - StringBuilder args = new StringBuilder(); - args.Append("--file-selection "); - if (!string.IsNullOrEmpty(options.PickerTitle)) - { - StringBuilder stringBuilder = args; - StringBuilder stringBuilder2 = stringBuilder; - StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(11, 1, stringBuilder); - handler.AppendLiteral("--title=\""); - handler.AppendFormatted(EscapeForShell(options.PickerTitle)); - handler.AppendLiteral("\" "); - stringBuilder2.Append(ref handler); - } - if (allowMultiple) - { - args.Append("--multiple --separator=\"|\" "); - } - List extensionsFromFileType = GetExtensionsFromFileType(options.FileTypes); - if (extensionsFromFileType.Count > 0) - { - string value = string.Join(" ", extensionsFromFileType.Select((string e) => "*" + e)); - StringBuilder stringBuilder = args; - StringBuilder stringBuilder3 = stringBuilder; - StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(25, 1, stringBuilder); - handler.AppendLiteral("--file-filter=\"Files | "); - handler.AppendFormatted(value); - handler.AppendLiteral("\" "); - stringBuilder3.Append(ref handler); - } - string text = await Task.Run(() => RunCommand("zenity", args.ToString())); - if (string.IsNullOrWhiteSpace(text)) - { - return Enumerable.Empty(); - } - return ((IEnumerable)text.Trim().Split('|', StringSplitOptions.RemoveEmptyEntries)).Select((Func)((string f) => new FileResult(f.Trim()))).ToList(); - } - - private async Task> PickWithKdialogAsync(PickOptions options, bool allowMultiple) - { - StringBuilder args = new StringBuilder(); - args.Append("--getopenfilename "); - args.Append(". "); - List extensionsFromFileType = GetExtensionsFromFileType(options.FileTypes); - if (extensionsFromFileType.Count > 0) - { - string value = string.Join(" ", extensionsFromFileType.Select((string e) => "*" + e)); - StringBuilder stringBuilder = args; - StringBuilder stringBuilder2 = stringBuilder; - StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(11, 1, stringBuilder); - handler.AppendLiteral("\"Files ("); - handler.AppendFormatted(value); - handler.AppendLiteral(")\" "); - stringBuilder2.Append(ref handler); - } - if (!string.IsNullOrEmpty(options.PickerTitle)) - { - StringBuilder stringBuilder = args; - StringBuilder stringBuilder3 = stringBuilder; - StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(11, 1, stringBuilder); - handler.AppendLiteral("--title \""); - handler.AppendFormatted(EscapeForShell(options.PickerTitle)); - handler.AppendLiteral("\" "); - stringBuilder3.Append(ref handler); - } - if (allowMultiple) - { - args.Append("--multiple --separate-output "); - } - string text = await Task.Run(() => RunCommand("kdialog", args.ToString())); - if (string.IsNullOrWhiteSpace(text)) - { - return Enumerable.Empty(); - } - return ((IEnumerable)text.Trim().Split('\n', StringSplitOptions.RemoveEmptyEntries)).Select((Func)((string f) => new FileResult(f.Trim()))).ToList(); - } - - private async Task> PickWithYadAsync(PickOptions options, bool allowMultiple) - { - StringBuilder args = new StringBuilder(); - args.Append("--file "); - if (!string.IsNullOrEmpty(options.PickerTitle)) - { - StringBuilder stringBuilder = args; - StringBuilder stringBuilder2 = stringBuilder; - StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(11, 1, stringBuilder); - handler.AppendLiteral("--title=\""); - handler.AppendFormatted(EscapeForShell(options.PickerTitle)); - handler.AppendLiteral("\" "); - stringBuilder2.Append(ref handler); - } - if (allowMultiple) - { - args.Append("--multiple --separator=\"|\" "); - } - List extensionsFromFileType = GetExtensionsFromFileType(options.FileTypes); - if (extensionsFromFileType.Count > 0) - { - string value = string.Join(" ", extensionsFromFileType.Select((string e) => "*" + e)); - StringBuilder stringBuilder = args; - StringBuilder stringBuilder3 = stringBuilder; - StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(25, 1, stringBuilder); - handler.AppendLiteral("--file-filter=\"Files | "); - handler.AppendFormatted(value); - handler.AppendLiteral("\" "); - stringBuilder3.Append(ref handler); - } - string text = await Task.Run(() => RunCommand("yad", args.ToString())); - if (string.IsNullOrWhiteSpace(text)) - { - return Enumerable.Empty(); - } - return ((IEnumerable)text.Trim().Split('|', StringSplitOptions.RemoveEmptyEntries)).Select((Func)((string f) => new FileResult(f.Trim()))).ToList(); - } - - private List GetExtensionsFromFileType(FilePickerFileType? fileType) - { - List list = new List(); - if (fileType == null) - { - return list; - } - try - { - IEnumerable value = fileType.Value; - if (value == null) - { - return list; - } - foreach (string item2 in value) - { - if (item2.StartsWith(".") || (!item2.Contains('/') && !item2.Contains('*'))) - { - string item = (item2.StartsWith(".") ? item2 : ("." + item2)); - if (!list.Contains(item)) - { - list.Add(item); - } - } - } - } - catch - { - } - return list; - } - - private string? BuildPortalFilterArgs(FilePickerFileType? fileType) - { - List extensionsFromFileType = GetExtensionsFromFileType(fileType); - if (extensionsFromFileType.Count == 0) - { - return null; - } - string text = string.Join(", ", extensionsFromFileType.Select((string e) => "(uint32 0, '*" + e + "')")); - return "[('Files', [" + text + "])]"; - } - - private string? ParseRequestPath(string output) - { - int num = output.IndexOf("'/"); - int num2 = output.IndexOf("',", num); - if (num >= 0 && num2 > num) - { - return output.Substring(num + 1, num2 - num - 1); - } - return null; - } - - private string EscapeForShell(string input) - { - return input.Replace("\"", "\\\"").Replace("'", "\\'"); - } - - private string RunCommand(string command, string arguments) - { - try - { - using Process process = new Process - { - StartInfo = new ProcessStartInfo - { - FileName = command, - Arguments = arguments, - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - CreateNoWindow = true - } - }; - process.Start(); - string result = process.StandardOutput.ReadToEnd(); - process.WaitForExit(30000); - return result; - } - catch (Exception ex) - { - Console.WriteLine("[FilePickerService] Command error: " + ex.Message); - return ""; - } - } -} diff --git a/Services/PortalFolderPickerService.cs b/Services/PortalFolderPickerService.cs deleted file mode 100644 index fc33b4b..0000000 --- a/Services/PortalFolderPickerService.cs +++ /dev/null @@ -1,76 +0,0 @@ -using System.Diagnostics; -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.Maui.Platform.Linux.Services; - -public class PortalFolderPickerService -{ - public async Task PickAsync(FolderPickerOptions? options = null, CancellationToken cancellationToken = default(CancellationToken)) - { - if (options == null) - { - options = new FolderPickerOptions(); - } - string text = null; - if (IsCommandAvailable("zenity")) - { - string args = "--file-selection --directory --title=\"" + (options.Title ?? "Select Folder") + "\""; - text = await Task.Run(() => RunCommand("zenity", args)?.Trim()); - } - else if (IsCommandAvailable("kdialog")) - { - string args2 = "--getexistingdirectory . --title \"" + (options.Title ?? "Select Folder") + "\""; - text = await Task.Run(() => RunCommand("kdialog", args2)?.Trim()); - } - if (!string.IsNullOrEmpty(text) && Directory.Exists(text)) - { - return new FolderPickerResult(new FolderResult(text)); - } - return new FolderPickerResult(null); - } - - public async Task PickAsync(CancellationToken cancellationToken = default(CancellationToken)) - { - return await PickAsync(null, cancellationToken); - } - - private bool IsCommandAvailable(string command) - { - try - { - return !string.IsNullOrWhiteSpace(RunCommand("which", command)); - } - catch - { - return false; - } - } - - private string? RunCommand(string command, string arguments) - { - try - { - using Process process = new Process - { - StartInfo = new ProcessStartInfo - { - FileName = command, - Arguments = arguments, - RedirectStandardOutput = true, - UseShellExecute = false, - CreateNoWindow = true - } - }; - process.Start(); - string result = process.StandardOutput.ReadToEnd(); - process.WaitForExit(30000); - return result; - } - catch - { - return null; - } - } -} diff --git a/Services/PreEditAttribute.cs b/Services/PreEditAttribute.cs deleted file mode 100644 index 5154ac7..0000000 --- a/Services/PreEditAttribute.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Microsoft.Maui.Platform.Linux.Services; - -public class PreEditAttribute -{ - public int Start { get; set; } - - public int Length { get; set; } - - public PreEditAttributeType Type { get; set; } -} diff --git a/Services/PreEditAttributeType.cs b/Services/PreEditAttributeType.cs deleted file mode 100644 index 90ecdf5..0000000 --- a/Services/PreEditAttributeType.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Microsoft.Maui.Platform.Linux.Services; - -public enum PreEditAttributeType -{ - None, - Underline, - Highlighted, - Reverse -} diff --git a/Services/PreEditChangedEventArgs.cs b/Services/PreEditChangedEventArgs.cs deleted file mode 100644 index 149697c..0000000 --- a/Services/PreEditChangedEventArgs.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Microsoft.Maui.Platform.Linux.Services; - -public class PreEditChangedEventArgs : EventArgs -{ - public string PreEditText { get; } - - public int CursorPosition { get; } - - public IReadOnlyList Attributes { get; } - - public PreEditChangedEventArgs(string preEditText, int cursorPosition, IReadOnlyList? attributes = null) - { - PreEditText = preEditText; - CursorPosition = cursorPosition; - Attributes = attributes ?? Array.Empty(); - } -} diff --git a/Services/PreferencesService.cs b/Services/PreferencesService.cs index 7c33d2c..02fa1f6 100644 --- a/Services/PreferencesService.cs +++ b/Services/PreferencesService.cs @@ -1,195 +1,201 @@ -using System; -using System.Collections.Generic; -using System.IO; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using System.Text.Json; -using Microsoft.Maui.ApplicationModel; using Microsoft.Maui.Storage; +using MauiAppInfo = Microsoft.Maui.ApplicationModel.AppInfo; namespace Microsoft.Maui.Platform.Linux.Services; +/// +/// Linux preferences implementation using JSON file storage. +/// Follows XDG Base Directory Specification. +/// public class PreferencesService : IPreferences { - private readonly string _preferencesPath; + private readonly string _preferencesPath; + private readonly object _lock = new(); + private Dictionary> _preferences = new(); + private bool _loaded; - private readonly object _lock = new object(); + public PreferencesService() + { + // Use XDG config directory + var configHome = Environment.GetEnvironmentVariable("XDG_CONFIG_HOME"); + if (string.IsNullOrEmpty(configHome)) + { + configHome = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config"); + } - private Dictionary> _preferences = new Dictionary>(); + var appName = MauiAppInfo.Current?.Name ?? "MauiApp"; + var appDir = Path.Combine(configHome, appName); + Directory.CreateDirectory(appDir); - private bool _loaded; + _preferencesPath = Path.Combine(appDir, "preferences.json"); + } - public PreferencesService() - { - string text = Environment.GetEnvironmentVariable("XDG_CONFIG_HOME"); - if (string.IsNullOrEmpty(text)) - { - text = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config"); - } - IAppInfo current = AppInfo.Current; - string path = ((current != null) ? current.Name : null) ?? "MauiApp"; - string text2 = Path.Combine(text, path); - Directory.CreateDirectory(text2); - _preferencesPath = Path.Combine(text2, "preferences.json"); - } + private void EnsureLoaded() + { + if (_loaded) return; - private void EnsureLoaded() - { - if (_loaded) - { - return; - } - lock (_lock) - { - if (_loaded) - { - return; - } - try - { - if (File.Exists(_preferencesPath)) - { - string json = File.ReadAllText(_preferencesPath); - _preferences = JsonSerializer.Deserialize>>(json) ?? new Dictionary>(); - } - } - catch - { - _preferences = new Dictionary>(); - } - _loaded = true; - } - } + lock (_lock) + { + if (_loaded) return; - private void Save() - { - lock (_lock) - { - try - { - string contents = JsonSerializer.Serialize(_preferences, new JsonSerializerOptions - { - WriteIndented = true - }); - File.WriteAllText(_preferencesPath, contents); - } - catch - { - } - } - } + try + { + if (File.Exists(_preferencesPath)) + { + var json = File.ReadAllText(_preferencesPath); + _preferences = JsonSerializer.Deserialize>>(json) + ?? new(); + } + } + catch + { + _preferences = new(); + } - private Dictionary GetContainer(string? sharedName) - { - string key = sharedName ?? "__default__"; - EnsureLoaded(); - if (!_preferences.TryGetValue(key, out Dictionary value)) - { - value = new Dictionary(); - _preferences[key] = value; - } - return value; - } + _loaded = true; + } + } - public bool ContainsKey(string key, string? sharedName = null) - { - return GetContainer(sharedName).ContainsKey(key); - } + private void Save() + { + lock (_lock) + { + try + { + var json = JsonSerializer.Serialize(_preferences, new JsonSerializerOptions + { + WriteIndented = true + }); + File.WriteAllText(_preferencesPath, json); + } + catch + { + // Silently fail save operations + } + } + } - public void Remove(string key, string? sharedName = null) - { - lock (_lock) - { - if (GetContainer(sharedName).Remove(key)) - { - Save(); - } - } - } + private Dictionary GetContainer(string? sharedName) + { + var key = sharedName ?? "__default__"; - public void Clear(string? sharedName = null) - { - lock (_lock) - { - GetContainer(sharedName).Clear(); - Save(); - } - } + EnsureLoaded(); - public void Set(string key, T value, string? sharedName = null) - { - lock (_lock) - { - GetContainer(sharedName)[key] = value; - Save(); - } - } + if (!_preferences.TryGetValue(key, out var container)) + { + container = new Dictionary(); + _preferences[key] = container; + } - public T Get(string key, T defaultValue, string? sharedName = null) - { - if (!GetContainer(sharedName).TryGetValue(key, out object value)) - { - return defaultValue; - } - if (value == null) - { - return defaultValue; - } - try - { - if (value is JsonElement element) - { - return ConvertJsonElement(element, defaultValue); - } - if (value is T result) - { - return result; - } - return (T)Convert.ChangeType(value, typeof(T)); - } - catch - { - return defaultValue; - } - } + return container; + } - private T ConvertJsonElement(JsonElement element, T defaultValue) - { - Type typeFromHandle = typeof(T); - try - { - if (typeFromHandle == typeof(string)) - { - return (T)(object)element.GetString(); - } - if (typeFromHandle == typeof(int)) - { - return (T)(object)element.GetInt32(); - } - if (typeFromHandle == typeof(long)) - { - return (T)(object)element.GetInt64(); - } - if (typeFromHandle == typeof(float)) - { - return (T)(object)element.GetSingle(); - } - if (typeFromHandle == typeof(double)) - { - return (T)(object)element.GetDouble(); - } - if (typeFromHandle == typeof(bool)) - { - return (T)(object)element.GetBoolean(); - } - if (typeFromHandle == typeof(DateTime)) - { - return (T)(object)element.GetDateTime(); - } - T val = element.Deserialize(); - return (T)((val != null) ? ((object)val) : ((object)defaultValue)); - } - catch - { - return defaultValue; - } - } + public bool ContainsKey(string key, string? sharedName = null) + { + var container = GetContainer(sharedName); + return container.ContainsKey(key); + } + + public void Remove(string key, string? sharedName = null) + { + lock (_lock) + { + var container = GetContainer(sharedName); + if (container.Remove(key)) + { + Save(); + } + } + } + + public void Clear(string? sharedName = null) + { + lock (_lock) + { + var container = GetContainer(sharedName); + container.Clear(); + Save(); + } + } + + public void Set(string key, T value, string? sharedName = null) + { + lock (_lock) + { + var container = GetContainer(sharedName); + container[key] = value; + Save(); + } + } + + public T Get(string key, T defaultValue, string? sharedName = null) + { + var container = GetContainer(sharedName); + + if (!container.TryGetValue(key, out var value)) + return defaultValue; + + if (value == null) + return defaultValue; + + try + { + // Handle JsonElement conversion (from deserialization) + if (value is JsonElement element) + { + return ConvertJsonElement(element, defaultValue); + } + + // Direct conversion + if (value is T typedValue) + return typedValue; + + // Try Convert.ChangeType for primitive types + return (T)Convert.ChangeType(value, typeof(T)); + } + catch + { + return defaultValue; + } + } + + private T ConvertJsonElement(JsonElement element, T defaultValue) + { + var targetType = typeof(T); + + try + { + if (targetType == typeof(string)) + return (T)(object)element.GetString()!; + + if (targetType == typeof(int)) + return (T)(object)element.GetInt32(); + + if (targetType == typeof(long)) + return (T)(object)element.GetInt64(); + + if (targetType == typeof(float)) + return (T)(object)element.GetSingle(); + + if (targetType == typeof(double)) + return (T)(object)element.GetDouble(); + + if (targetType == typeof(bool)) + return (T)(object)element.GetBoolean(); + + if (targetType == typeof(DateTime)) + return (T)(object)element.GetDateTime(); + + // For complex types, deserialize + return element.Deserialize() ?? defaultValue; + } + catch + { + return defaultValue; + } + } } diff --git a/Services/ScaleChangedEventArgs.cs b/Services/ScaleChangedEventArgs.cs deleted file mode 100644 index 8dd0e7b..0000000 --- a/Services/ScaleChangedEventArgs.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; - -namespace Microsoft.Maui.Platform.Linux.Services; - -public class ScaleChangedEventArgs : EventArgs -{ - public float OldScale { get; } - - public float NewScale { get; } - - public float NewDpi { get; } - - public ScaleChangedEventArgs(float oldScale, float newScale, float newDpi) - { - OldScale = oldScale; - NewScale = newScale; - NewDpi = newDpi; - } -} diff --git a/Services/SecureStorageService.cs b/Services/SecureStorageService.cs index 99d6fe8..8496121 100644 --- a/Services/SecureStorageService.cs +++ b/Services/SecureStorageService.cs @@ -1,304 +1,359 @@ -using System; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using System.Diagnostics; -using System.IO; using System.Security.Cryptography; using System.Text; -using System.Threading.Tasks; using Microsoft.Maui.Storage; namespace Microsoft.Maui.Platform.Linux.Services; +/// +/// Linux secure storage implementation using secret-tool (libsecret) or encrypted file fallback. +/// public class SecureStorageService : ISecureStorage { - private const string ServiceName = "maui-secure-storage"; + private const string ServiceName = "maui-secure-storage"; + private const string FallbackDirectory = ".maui-secure"; + private readonly string _fallbackPath; + private readonly bool _useSecretService; - private const string FallbackDirectory = ".maui-secure"; + public SecureStorageService() + { + _fallbackPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + FallbackDirectory); + _useSecretService = CheckSecretServiceAvailable(); + } - private readonly string _fallbackPath; + private bool CheckSecretServiceAvailable() + { + try + { + var startInfo = new ProcessStartInfo + { + FileName = "which", + Arguments = "secret-tool", + UseShellExecute = false, + RedirectStandardOutput = true, + CreateNoWindow = true + }; - private readonly bool _useSecretService; + using var process = Process.Start(startInfo); + if (process == null) return false; - public SecureStorageService() - { - _fallbackPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".maui-secure"); - _useSecretService = CheckSecretServiceAvailable(); - } + process.WaitForExit(); + return process.ExitCode == 0; + } + catch + { + return false; + } + } - private bool CheckSecretServiceAvailable() - { - try - { - using Process process = Process.Start(new ProcessStartInfo - { - FileName = "which", - Arguments = "secret-tool", - UseShellExecute = false, - RedirectStandardOutput = true, - CreateNoWindow = true - }); - if (process == null) - { - return false; - } - process.WaitForExit(); - return process.ExitCode == 0; - } - catch - { - return false; - } - } + public Task GetAsync(string key) + { + if (string.IsNullOrEmpty(key)) + throw new ArgumentNullException(nameof(key)); - public Task GetAsync(string key) - { - if (string.IsNullOrEmpty(key)) - { - throw new ArgumentNullException("key"); - } - if (_useSecretService) - { - return GetFromSecretServiceAsync(key); - } - return GetFromFallbackAsync(key); - } + if (_useSecretService) + { + return GetFromSecretServiceAsync(key); + } + else + { + return GetFromFallbackAsync(key); + } + } - public Task SetAsync(string key, string value) - { - if (string.IsNullOrEmpty(key)) - { - throw new ArgumentNullException("key"); - } - if (_useSecretService) - { - return SetInSecretServiceAsync(key, value); - } - return SetInFallbackAsync(key, value); - } + public Task SetAsync(string key, string value) + { + if (string.IsNullOrEmpty(key)) + throw new ArgumentNullException(nameof(key)); - public bool Remove(string key) - { - if (string.IsNullOrEmpty(key)) - { - throw new ArgumentNullException("key"); - } - if (_useSecretService) - { - return RemoveFromSecretService(key); - } - return RemoveFromFallback(key); - } + if (_useSecretService) + { + return SetInSecretServiceAsync(key, value); + } + else + { + return SetInFallbackAsync(key, value); + } + } - public void RemoveAll() - { - if (!_useSecretService && Directory.Exists(_fallbackPath)) - { - Directory.Delete(_fallbackPath, recursive: true); - } - } + public bool Remove(string key) + { + if (string.IsNullOrEmpty(key)) + throw new ArgumentNullException(nameof(key)); - private async Task GetFromSecretServiceAsync(string key) - { - _ = 1; - try - { - ProcessStartInfo startInfo = new ProcessStartInfo - { - FileName = "secret-tool", - Arguments = "lookup service maui-secure-storage key " + EscapeArg(key), - UseShellExecute = false, - RedirectStandardOutput = true, - RedirectStandardError = true, - CreateNoWindow = true - }; - using Process process = Process.Start(startInfo); - if (process == null) - { - return null; - } - string output = await process.StandardOutput.ReadToEndAsync(); - await process.WaitForExitAsync(); - if (process.ExitCode == 0 && !string.IsNullOrEmpty(output)) - { - return output.TrimEnd('\n'); - } - return null; - } - catch - { - return null; - } - } + if (_useSecretService) + { + return RemoveFromSecretService(key); + } + else + { + return RemoveFromFallback(key); + } + } - private async Task SetInSecretServiceAsync(string key, string value) - { - try - { - ProcessStartInfo startInfo = new ProcessStartInfo - { - FileName = "secret-tool", - Arguments = $"store --label=\"{EscapeArg(key)}\" service {"maui-secure-storage"} key {EscapeArg(key)}", - UseShellExecute = false, - RedirectStandardInput = true, - RedirectStandardError = true, - CreateNoWindow = true - }; - using Process process = Process.Start(startInfo); - if (process == null) - { - throw new InvalidOperationException("Failed to start secret-tool"); - } - await process.StandardInput.WriteAsync(value); - process.StandardInput.Close(); - await process.WaitForExitAsync(); - if (process.ExitCode != 0) - { - throw new InvalidOperationException("Failed to store secret: " + await process.StandardError.ReadToEndAsync()); - } - } - catch (Exception ex) when (!(ex is InvalidOperationException)) - { - await SetInFallbackAsync(key, value); - } - } + public void RemoveAll() + { + if (_useSecretService) + { + // Cannot easily remove all from secret service without knowing all keys + // This would require additional tracking + } + else + { + if (Directory.Exists(_fallbackPath)) + { + Directory.Delete(_fallbackPath, true); + } + } + } - private bool RemoveFromSecretService(string key) - { - try - { - using Process process = Process.Start(new ProcessStartInfo - { - FileName = "secret-tool", - Arguments = "clear service maui-secure-storage key " + EscapeArg(key), - UseShellExecute = false, - CreateNoWindow = true - }); - if (process == null) - { - return false; - } - process.WaitForExit(); - return process.ExitCode == 0; - } - catch - { - return false; - } - } + #region Secret Service (libsecret) - private async Task GetFromFallbackAsync(string key) - { - string fallbackFilePath = GetFallbackFilePath(key); - if (!File.Exists(fallbackFilePath)) - { - return null; - } - try - { - return DecryptData(await File.ReadAllBytesAsync(fallbackFilePath)); - } - catch - { - return null; - } - } + private async Task GetFromSecretServiceAsync(string key) + { + try + { + var startInfo = new ProcessStartInfo + { + FileName = "secret-tool", + Arguments = $"lookup service {ServiceName} key {EscapeArg(key)}", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; - private async Task SetInFallbackAsync(string key, string value) - { - EnsureFallbackDirectory(); - string filePath = GetFallbackFilePath(key); - byte[] bytes = EncryptData(value); - await File.WriteAllBytesAsync(filePath, bytes); - File.SetUnixFileMode(filePath, UnixFileMode.UserWrite | UnixFileMode.UserRead); - } + using var process = Process.Start(startInfo); + if (process == null) return null; - private bool RemoveFromFallback(string key) - { - string fallbackFilePath = GetFallbackFilePath(key); - if (File.Exists(fallbackFilePath)) - { - File.Delete(fallbackFilePath); - return true; - } - return false; - } + var output = await process.StandardOutput.ReadToEndAsync(); + await process.WaitForExitAsync(); - private string GetFallbackFilePath(string key) - { - using SHA256 sHA = SHA256.Create(); - string path = Convert.ToHexString(sHA.ComputeHash(Encoding.UTF8.GetBytes(key))).ToLowerInvariant(); - return Path.Combine(_fallbackPath, path); - } + if (process.ExitCode == 0 && !string.IsNullOrEmpty(output)) + { + return output.TrimEnd('\n'); + } - private void EnsureFallbackDirectory() - { - if (!Directory.Exists(_fallbackPath)) - { - Directory.CreateDirectory(_fallbackPath); - File.SetUnixFileMode(_fallbackPath, UnixFileMode.UserExecute | UnixFileMode.UserWrite | UnixFileMode.UserRead); - } - } + return null; + } + catch + { + return null; + } + } - private byte[] EncryptData(string data) - { - byte[] machineKey = GetMachineKey(); - using Aes aes = Aes.Create(); - aes.Key = machineKey; - aes.GenerateIV(); - using ICryptoTransform cryptoTransform = aes.CreateEncryptor(); - byte[] bytes = Encoding.UTF8.GetBytes(data); - byte[] array = cryptoTransform.TransformFinalBlock(bytes, 0, bytes.Length); - byte[] array2 = new byte[aes.IV.Length + array.Length]; - Buffer.BlockCopy(aes.IV, 0, array2, 0, aes.IV.Length); - Buffer.BlockCopy(array, 0, array2, aes.IV.Length, array.Length); - return array2; - } + private async Task SetInSecretServiceAsync(string key, string value) + { + try + { + var startInfo = new ProcessStartInfo + { + FileName = "secret-tool", + Arguments = $"store --label=\"{EscapeArg(key)}\" service {ServiceName} key {EscapeArg(key)}", + UseShellExecute = false, + RedirectStandardInput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; - private string DecryptData(byte[] encryptedData) - { - byte[] machineKey = GetMachineKey(); - using Aes aes = Aes.Create(); - aes.Key = machineKey; - byte[] array = new byte[aes.BlockSize / 8]; - Buffer.BlockCopy(encryptedData, 0, array, 0, array.Length); - aes.IV = array; - byte[] array2 = new byte[encryptedData.Length - array.Length]; - Buffer.BlockCopy(encryptedData, array.Length, array2, 0, array2.Length); - using ICryptoTransform cryptoTransform = aes.CreateDecryptor(); - byte[] bytes = cryptoTransform.TransformFinalBlock(array2, 0, array2.Length); - return Encoding.UTF8.GetString(bytes); - } + using var process = Process.Start(startInfo); + if (process == null) + throw new InvalidOperationException("Failed to start secret-tool"); - private byte[] GetMachineKey() - { - string machineId = GetMachineId(); - string userName = Environment.UserName; - string s = $"{machineId}:{userName}:{"maui-secure-storage"}"; - using SHA256 sHA = SHA256.Create(); - return sHA.ComputeHash(Encoding.UTF8.GetBytes(s)); - } + await process.StandardInput.WriteAsync(value); + process.StandardInput.Close(); - private string GetMachineId() - { - try - { - if (File.Exists("/etc/machine-id")) - { - return File.ReadAllText("/etc/machine-id").Trim(); - } - if (File.Exists("/var/lib/dbus/machine-id")) - { - return File.ReadAllText("/var/lib/dbus/machine-id").Trim(); - } - return Environment.MachineName; - } - catch - { - return Environment.MachineName; - } - } + await process.WaitForExitAsync(); - private static string EscapeArg(string arg) - { - return arg.Replace("\"", "\\\"").Replace("'", "\\'"); - } + if (process.ExitCode != 0) + { + var error = await process.StandardError.ReadToEndAsync(); + throw new InvalidOperationException($"Failed to store secret: {error}"); + } + } + catch (Exception ex) when (ex is not InvalidOperationException) + { + // Fall back to file storage + await SetInFallbackAsync(key, value); + } + } + + private bool RemoveFromSecretService(string key) + { + try + { + var startInfo = new ProcessStartInfo + { + FileName = "secret-tool", + Arguments = $"clear service {ServiceName} key {EscapeArg(key)}", + UseShellExecute = false, + CreateNoWindow = true + }; + + using var process = Process.Start(startInfo); + if (process == null) return false; + + process.WaitForExit(); + return process.ExitCode == 0; + } + catch + { + return false; + } + } + + #endregion + + #region Fallback Encrypted Storage + + private async Task GetFromFallbackAsync(string key) + { + var filePath = GetFallbackFilePath(key); + if (!File.Exists(filePath)) + return null; + + try + { + var encryptedData = await File.ReadAllBytesAsync(filePath); + return DecryptData(encryptedData); + } + catch + { + return null; + } + } + + private async Task SetInFallbackAsync(string key, string value) + { + EnsureFallbackDirectory(); + + var filePath = GetFallbackFilePath(key); + var encryptedData = EncryptData(value); + + await File.WriteAllBytesAsync(filePath, encryptedData); + + // Set restrictive permissions + File.SetUnixFileMode(filePath, UnixFileMode.UserRead | UnixFileMode.UserWrite); + } + + private bool RemoveFromFallback(string key) + { + var filePath = GetFallbackFilePath(key); + if (File.Exists(filePath)) + { + File.Delete(filePath); + return true; + } + return false; + } + + private string GetFallbackFilePath(string key) + { + // Hash the key to create a safe filename + using var sha256 = SHA256.Create(); + var hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(key)); + var fileName = Convert.ToHexString(hash).ToLowerInvariant(); + return Path.Combine(_fallbackPath, fileName); + } + + private void EnsureFallbackDirectory() + { + if (!Directory.Exists(_fallbackPath)) + { + Directory.CreateDirectory(_fallbackPath); + // Set restrictive permissions on the directory + File.SetUnixFileMode(_fallbackPath, + UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute); + } + } + + private byte[] EncryptData(string data) + { + // Use a machine-specific key derived from machine ID + var key = GetMachineKey(); + + using var aes = Aes.Create(); + aes.Key = key; + aes.GenerateIV(); + + using var encryptor = aes.CreateEncryptor(); + var plainBytes = Encoding.UTF8.GetBytes(data); + var encryptedBytes = encryptor.TransformFinalBlock(plainBytes, 0, plainBytes.Length); + + // Prepend IV to encrypted data + var result = new byte[aes.IV.Length + encryptedBytes.Length]; + Buffer.BlockCopy(aes.IV, 0, result, 0, aes.IV.Length); + Buffer.BlockCopy(encryptedBytes, 0, result, aes.IV.Length, encryptedBytes.Length); + + return result; + } + + private string DecryptData(byte[] encryptedData) + { + var key = GetMachineKey(); + + using var aes = Aes.Create(); + aes.Key = key; + + // Extract IV from beginning of data + var iv = new byte[aes.BlockSize / 8]; + Buffer.BlockCopy(encryptedData, 0, iv, 0, iv.Length); + aes.IV = iv; + + var cipherText = new byte[encryptedData.Length - iv.Length]; + Buffer.BlockCopy(encryptedData, iv.Length, cipherText, 0, cipherText.Length); + + using var decryptor = aes.CreateDecryptor(); + var plainBytes = decryptor.TransformFinalBlock(cipherText, 0, cipherText.Length); + + return Encoding.UTF8.GetString(plainBytes); + } + + private byte[] GetMachineKey() + { + // Derive a key from machine-id and user + var machineId = GetMachineId(); + var user = Environment.UserName; + var combined = $"{machineId}:{user}:{ServiceName}"; + + using var sha256 = SHA256.Create(); + return sha256.ComputeHash(Encoding.UTF8.GetBytes(combined)); + } + + private string GetMachineId() + { + try + { + // Try /etc/machine-id first (systemd) + if (File.Exists("/etc/machine-id")) + { + return File.ReadAllText("/etc/machine-id").Trim(); + } + + // Try /var/lib/dbus/machine-id (older systems) + if (File.Exists("/var/lib/dbus/machine-id")) + { + return File.ReadAllText("/var/lib/dbus/machine-id").Trim(); + } + + // Fallback to hostname + return Environment.MachineName; + } + catch + { + return Environment.MachineName; + } + } + + #endregion + + private static string EscapeArg(string arg) + { + return arg.Replace("\"", "\\\"").Replace("'", "\\'"); + } } diff --git a/Services/ShareService.cs b/Services/ShareService.cs index df47bc3..6e1bf2c 100644 --- a/Services/ShareService.cs +++ b/Services/ShareService.cs @@ -1,139 +1,147 @@ -using System; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Threading.Tasks; using Microsoft.Maui.ApplicationModel.DataTransfer; -using Microsoft.Maui.Storage; namespace Microsoft.Maui.Platform.Linux.Services; +/// +/// Linux share implementation using xdg-open and portal APIs. +/// public class ShareService : IShare { - public async Task RequestAsync(ShareTextRequest request) - { - if (request == null) - { - throw new ArgumentNullException("request"); - } - if (!string.IsNullOrEmpty(request.Uri)) - { - await OpenUrlAsync(request.Uri); - } - else if (!string.IsNullOrEmpty(request.Text)) - { - string text = Uri.EscapeDataString(request.Subject ?? ""); - string text2 = Uri.EscapeDataString(request.Text ?? ""); - string url = "mailto:?subject=" + text + "&body=" + text2; - await OpenUrlAsync(url); - } - } + public async Task RequestAsync(ShareTextRequest request) + { + if (request == null) + throw new ArgumentNullException(nameof(request)); - public async Task RequestAsync(ShareFileRequest request) - { - if (request == null) - { - throw new ArgumentNullException("request"); - } - if (request.File == null) - { - throw new ArgumentException("File is required", "request"); - } - await ShareFileAsync(((FileBase)request.File).FullPath); - } + // On Linux, we can use mailto: for text sharing or write to a temp file + if (!string.IsNullOrEmpty(request.Uri)) + { + // Share as URL + await OpenUrlAsync(request.Uri); + } + else if (!string.IsNullOrEmpty(request.Text)) + { + // Try to use email for text sharing + var subject = Uri.EscapeDataString(request.Subject ?? ""); + var body = Uri.EscapeDataString(request.Text ?? ""); + var mailto = $"mailto:?subject={subject}&body={body}"; + await OpenUrlAsync(mailto); + } + } - public async Task RequestAsync(ShareMultipleFilesRequest request) - { - if (request == null) - { - throw new ArgumentNullException("request"); - } - if (request.Files == null || !request.Files.Any()) - { - throw new ArgumentException("Files are required", "request"); - } - foreach (ShareFile file in request.Files) - { - await ShareFileAsync(((FileBase)file).FullPath); - } - } + public async Task RequestAsync(ShareFileRequest request) + { + if (request == null) + throw new ArgumentNullException(nameof(request)); - private async Task OpenUrlAsync(string url) - { - try - { - ProcessStartInfo startInfo = new ProcessStartInfo - { - FileName = "xdg-open", - Arguments = "\"" + url + "\"", - UseShellExecute = false, - CreateNoWindow = true - }; - using Process process = Process.Start(startInfo); - if (process != null) - { - await process.WaitForExitAsync(); - } - } - catch (Exception innerException) - { - throw new InvalidOperationException("Failed to open URL for sharing", innerException); - } - } + if (request.File == null) + throw new ArgumentException("File is required", nameof(request)); - private async Task ShareFileAsync(string filePath) - { - if (!File.Exists(filePath)) - { - throw new FileNotFoundException("File not found for sharing", filePath); - } - try - { - if (await TryPortalShareAsync(filePath)) - { - return; - } - ProcessStartInfo startInfo = new ProcessStartInfo - { - FileName = "xdg-open", - Arguments = "\"" + Path.GetDirectoryName(filePath) + "\"", - UseShellExecute = false, - CreateNoWindow = true - }; - using Process process = Process.Start(startInfo); - if (process != null) - { - await process.WaitForExitAsync(); - } - } - catch (Exception innerException) - { - throw new InvalidOperationException("Failed to share file", innerException); - } - } + await ShareFileAsync(request.File.FullPath); + } - private async Task TryPortalShareAsync(string filePath) - { - try - { - ProcessStartInfo startInfo = new ProcessStartInfo - { - FileName = "zenity", - Arguments = $"--info --text=\"File ready to share:\\n{Path.GetFileName(filePath)}\\n\\nPath: {filePath}\" --title=\"Share File\"", - UseShellExecute = false, - CreateNoWindow = true - }; - using Process process = Process.Start(startInfo); - if (process != null) - { - await process.WaitForExitAsync(); - return true; - } - return false; - } - catch - { - return false; - } - } + public async Task RequestAsync(ShareMultipleFilesRequest request) + { + if (request == null) + throw new ArgumentNullException(nameof(request)); + + if (request.Files == null || !request.Files.Any()) + throw new ArgumentException("Files are required", nameof(request)); + + // Share files one by one or use file manager + foreach (var file in request.Files) + { + await ShareFileAsync(file.FullPath); + } + } + + private async Task OpenUrlAsync(string url) + { + try + { + var startInfo = new ProcessStartInfo + { + FileName = "xdg-open", + Arguments = $"\"{url}\"", + UseShellExecute = false, + CreateNoWindow = true + }; + + using var process = Process.Start(startInfo); + if (process != null) + { + await process.WaitForExitAsync(); + } + } + catch (Exception ex) + { + throw new InvalidOperationException("Failed to open URL for sharing", ex); + } + } + + private async Task ShareFileAsync(string filePath) + { + if (!File.Exists(filePath)) + throw new FileNotFoundException("File not found for sharing", filePath); + + try + { + // Try to use the portal API via gdbus for proper share dialog + var portalResult = await TryPortalShareAsync(filePath); + if (portalResult) + return; + + // Fall back to opening with default file manager + var startInfo = new ProcessStartInfo + { + FileName = "xdg-open", + Arguments = $"\"{Path.GetDirectoryName(filePath)}\"", + UseShellExecute = false, + CreateNoWindow = true + }; + + using var process = Process.Start(startInfo); + if (process != null) + { + await process.WaitForExitAsync(); + } + } + catch (Exception ex) + { + throw new InvalidOperationException("Failed to share file", ex); + } + } + + private async Task TryPortalShareAsync(string filePath) + { + try + { + // Try freedesktop portal for proper share dialog + // This would use org.freedesktop.portal.FileChooser or similar + // For now, we'll use zenity --info as a fallback notification + + var startInfo = new ProcessStartInfo + { + FileName = "zenity", + Arguments = $"--info --text=\"File ready to share:\\n{Path.GetFileName(filePath)}\\n\\nPath: {filePath}\" --title=\"Share File\"", + UseShellExecute = false, + CreateNoWindow = true + }; + + using var process = Process.Start(startInfo); + if (process != null) + { + await process.WaitForExitAsync(); + return true; + } + return false; + } + catch + { + return false; + } + } } diff --git a/Services/SystemColors.cs b/Services/SystemColors.cs deleted file mode 100644 index 1235b5a..0000000 --- a/Services/SystemColors.cs +++ /dev/null @@ -1,26 +0,0 @@ -using SkiaSharp; - -namespace Microsoft.Maui.Platform.Linux.Services; - -public class SystemColors -{ - public SKColor Background { get; init; } - - public SKColor Surface { get; init; } - - public SKColor Primary { get; init; } - - public SKColor OnPrimary { get; init; } - - public SKColor Text { get; init; } - - public SKColor TextSecondary { get; init; } - - public SKColor Border { get; init; } - - public SKColor Divider { get; init; } - - public SKColor Error { get; init; } - - public SKColor Success { get; init; } -} diff --git a/Services/SystemTheme.cs b/Services/SystemTheme.cs deleted file mode 100644 index eaf8d7e..0000000 --- a/Services/SystemTheme.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Microsoft.Maui.Platform.Linux.Services; - -public enum SystemTheme -{ - Light, - Dark -} diff --git a/Services/SystemThemeService.cs b/Services/SystemThemeService.cs deleted file mode 100644 index 7077aef..0000000 --- a/Services/SystemThemeService.cs +++ /dev/null @@ -1,448 +0,0 @@ -using System; -using System.Diagnostics; -using System.IO; -using System.Threading.Tasks; -using SkiaSharp; - -namespace Microsoft.Maui.Platform.Linux.Services; - -public class SystemThemeService -{ - private static SystemThemeService? _instance; - - private static readonly object _lock = new object(); - - private FileSystemWatcher? _settingsWatcher; - - public static SystemThemeService Instance - { - get - { - if (_instance == null) - { - lock (_lock) - { - if (_instance == null) - { - _instance = new SystemThemeService(); - } - } - } - return _instance; - } - } - - public SystemTheme CurrentTheme { get; private set; } - - public SKColor AccentColor { get; private set; } = new SKColor((byte)33, (byte)150, (byte)243); - - public DesktopEnvironment Desktop { get; private set; } - - public SystemColors Colors { get; private set; } - - public event EventHandler? ThemeChanged; - - private SystemThemeService() - { - //IL_000d: Unknown result type (might be due to invalid IL or missing references) - //IL_0012: Unknown result type (might be due to invalid IL or missing references) - DetectDesktopEnvironment(); - DetectTheme(); - UpdateColors(); - SetupWatcher(); - } - - private void DetectDesktopEnvironment() - { - string text = Environment.GetEnvironmentVariable("XDG_CURRENT_DESKTOP")?.ToLowerInvariant() ?? ""; - string text2 = Environment.GetEnvironmentVariable("DESKTOP_SESSION")?.ToLowerInvariant() ?? ""; - if (text.Contains("gnome") || text2.Contains("gnome")) - { - Desktop = DesktopEnvironment.GNOME; - } - else if (text.Contains("kde") || text.Contains("plasma") || text2.Contains("plasma")) - { - Desktop = DesktopEnvironment.KDE; - } - else if (text.Contains("xfce") || text2.Contains("xfce")) - { - Desktop = DesktopEnvironment.XFCE; - } - else if (text.Contains("mate") || text2.Contains("mate")) - { - Desktop = DesktopEnvironment.MATE; - } - else if (text.Contains("cinnamon") || text2.Contains("cinnamon")) - { - Desktop = DesktopEnvironment.Cinnamon; - } - else if (text.Contains("lxqt")) - { - Desktop = DesktopEnvironment.LXQt; - } - else if (text.Contains("lxde")) - { - Desktop = DesktopEnvironment.LXDE; - } - else - { - Desktop = DesktopEnvironment.Unknown; - } - } - - private void DetectTheme() - { - //IL_0071: Unknown result type (might be due to invalid IL or missing references) - //IL_0076: Unknown result type (might be due to invalid IL or missing references) - //IL_0095: Unknown result type (might be due to invalid IL or missing references) - //IL_007a: Unknown result type (might be due to invalid IL or missing references) - //IL_007f: Unknown result type (might be due to invalid IL or missing references) - //IL_008e: Unknown result type (might be due to invalid IL or missing references) - //IL_0093: Unknown result type (might be due to invalid IL or missing references) - CurrentTheme = (Desktop switch - { - DesktopEnvironment.GNOME => DetectGnomeTheme(), - DesktopEnvironment.KDE => DetectKdeTheme(), - DesktopEnvironment.XFCE => DetectXfceTheme(), - DesktopEnvironment.Cinnamon => DetectCinnamonTheme(), - _ => DetectGtkTheme(), - }).GetValueOrDefault(); - AccentColor = (SKColor)(Desktop switch - { - DesktopEnvironment.GNOME => GetGnomeAccentColor(), - DesktopEnvironment.KDE => GetKdeAccentColor(), - _ => new SKColor((byte)33, (byte)150, (byte)243), - }); - } - - private SystemTheme? DetectGnomeTheme() - { - try - { - string text = RunCommand("gsettings", "get org.gnome.desktop.interface color-scheme"); - if (text.Contains("prefer-dark")) - { - return SystemTheme.Dark; - } - if (text.Contains("prefer-light") || text.Contains("default")) - { - return SystemTheme.Light; - } - text = RunCommand("gsettings", "get org.gnome.desktop.interface gtk-theme"); - if (text.ToLowerInvariant().Contains("dark")) - { - return SystemTheme.Dark; - } - } - catch - { - } - return null; - } - - private SystemTheme? DetectKdeTheme() - { - try - { - string path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "kdeglobals"); - if (File.Exists(path)) - { - string text = File.ReadAllText(path); - if (text.Contains("BreezeDark", StringComparison.OrdinalIgnoreCase) || text.Contains("Dark", StringComparison.OrdinalIgnoreCase)) - { - return SystemTheme.Dark; - } - } - } - catch - { - } - return null; - } - - private SystemTheme? DetectXfceTheme() - { - try - { - if (RunCommand("xfconf-query", "-c xsettings -p /Net/ThemeName").ToLowerInvariant().Contains("dark")) - { - return SystemTheme.Dark; - } - } - catch - { - } - return DetectGtkTheme(); - } - - private SystemTheme? DetectCinnamonTheme() - { - try - { - if (RunCommand("gsettings", "get org.cinnamon.desktop.interface gtk-theme").ToLowerInvariant().Contains("dark")) - { - return SystemTheme.Dark; - } - } - catch - { - } - return null; - } - - private SystemTheme? DetectGtkTheme() - { - try - { - string path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "gtk-3.0", "settings.ini"); - if (File.Exists(path)) - { - string[] array = File.ReadAllText(path).Split('\n'); - foreach (string text in array) - { - if (text.StartsWith("gtk-theme-name=", StringComparison.OrdinalIgnoreCase) && text.Substring("gtk-theme-name=".Length).Trim().Contains("dark", StringComparison.OrdinalIgnoreCase)) - { - return SystemTheme.Dark; - } - if (text.StartsWith("gtk-application-prefer-dark-theme=", StringComparison.OrdinalIgnoreCase)) - { - string text2 = text.Substring("gtk-application-prefer-dark-theme=".Length).Trim(); - if (text2 == "1" || text2.Equals("true", StringComparison.OrdinalIgnoreCase)) - { - return SystemTheme.Dark; - } - } - } - } - } - catch - { - } - return null; - } - - private SKColor GetGnomeAccentColor() - { - //IL_021e: Unknown result type (might be due to invalid IL or missing references) - //IL_0223: Unknown result type (might be due to invalid IL or missing references) - //IL_0206: Unknown result type (might be due to invalid IL or missing references) - //IL_020b: Unknown result type (might be due to invalid IL or missing references) - //IL_0227: Unknown result type (might be due to invalid IL or missing references) - //IL_020c: Unknown result type (might be due to invalid IL or missing references) - //IL_020d: Unknown result type (might be due to invalid IL or missing references) - //IL_01bc: Unknown result type (might be due to invalid IL or missing references) - //IL_01c1: Unknown result type (might be due to invalid IL or missing references) - //IL_015c: Unknown result type (might be due to invalid IL or missing references) - //IL_0161: Unknown result type (might be due to invalid IL or missing references) - //IL_0187: Unknown result type (might be due to invalid IL or missing references) - //IL_018c: Unknown result type (might be due to invalid IL or missing references) - //IL_01ab: Unknown result type (might be due to invalid IL or missing references) - //IL_01b0: Unknown result type (might be due to invalid IL or missing references) - //IL_01d0: Unknown result type (might be due to invalid IL or missing references) - //IL_01d5: Unknown result type (might be due to invalid IL or missing references) - //IL_01f2: Unknown result type (might be due to invalid IL or missing references) - //IL_01f7: Unknown result type (might be due to invalid IL or missing references) - //IL_01e4: Unknown result type (might be due to invalid IL or missing references) - //IL_01e9: Unknown result type (might be due to invalid IL or missing references) - //IL_0173: Unknown result type (might be due to invalid IL or missing references) - //IL_0178: Unknown result type (might be due to invalid IL or missing references) - //IL_019b: Unknown result type (might be due to invalid IL or missing references) - //IL_01a0: Unknown result type (might be due to invalid IL or missing references) - try - { - return (SKColor)(RunCommand("gsettings", "get org.gnome.desktop.interface accent-color").Trim().Trim('\'') switch - { - "blue" => new SKColor((byte)53, (byte)132, (byte)228), - "teal" => new SKColor((byte)42, (byte)195, (byte)222), - "green" => new SKColor((byte)58, (byte)148, (byte)74), - "yellow" => new SKColor((byte)246, (byte)211, (byte)45), - "orange" => new SKColor(byte.MaxValue, (byte)120, (byte)0), - "red" => new SKColor((byte)224, (byte)27, (byte)36), - "pink" => new SKColor((byte)214, (byte)86, (byte)140), - "purple" => new SKColor((byte)145, (byte)65, (byte)172), - "slate" => new SKColor((byte)94, (byte)92, (byte)100), - _ => new SKColor((byte)33, (byte)150, (byte)243), - }); - } - catch - { - return new SKColor((byte)33, (byte)150, (byte)243); - } - } - - private SKColor GetKdeAccentColor() - { - //IL_00e8: Unknown result type (might be due to invalid IL or missing references) - //IL_00c1: Unknown result type (might be due to invalid IL or missing references) - //IL_00c6: Unknown result type (might be due to invalid IL or missing references) - //IL_00ee: Unknown result type (might be due to invalid IL or missing references) - try - { - string path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "kdeglobals"); - if (File.Exists(path)) - { - string[] array = File.ReadAllText(path).Split('\n'); - bool flag = false; - string[] array2 = array; - foreach (string text in array2) - { - if (text.StartsWith("[Colors:Header]")) - { - flag = true; - continue; - } - if (text.StartsWith("[") && flag) - { - break; - } - if (flag && text.StartsWith("BackgroundNormal=")) - { - string[] array3 = text.Substring("BackgroundNormal=".Length).Split(','); - if (array3.Length >= 3 && byte.TryParse(array3[0], out var result) && byte.TryParse(array3[1], out var result2) && byte.TryParse(array3[2], out var result3)) - { - return new SKColor(result, result2, result3); - } - } - } - } - } - catch - { - } - return new SKColor((byte)33, (byte)150, (byte)243); - } - - private void UpdateColors() - { - //IL_00d9: Unknown result type (might be due to invalid IL or missing references) - //IL_00ea: Unknown result type (might be due to invalid IL or missing references) - //IL_00f6: Unknown result type (might be due to invalid IL or missing references) - //IL_0101: Unknown result type (might be due to invalid IL or missing references) - //IL_011b: Unknown result type (might be due to invalid IL or missing references) - //IL_0135: Unknown result type (might be due to invalid IL or missing references) - //IL_0146: Unknown result type (might be due to invalid IL or missing references) - //IL_0157: Unknown result type (might be due to invalid IL or missing references) - //IL_016b: Unknown result type (might be due to invalid IL or missing references) - //IL_0185: Unknown result type (might be due to invalid IL or missing references) - //IL_0022: Unknown result type (might be due to invalid IL or missing references) - //IL_002d: Unknown result type (might be due to invalid IL or missing references) - //IL_0039: Unknown result type (might be due to invalid IL or missing references) - //IL_0044: Unknown result type (might be due to invalid IL or missing references) - //IL_0055: Unknown result type (might be due to invalid IL or missing references) - //IL_0066: Unknown result type (might be due to invalid IL or missing references) - //IL_0080: Unknown result type (might be due to invalid IL or missing references) - //IL_009a: Unknown result type (might be due to invalid IL or missing references) - //IL_00ad: Unknown result type (might be due to invalid IL or missing references) - //IL_00be: Unknown result type (might be due to invalid IL or missing references) - Colors = ((CurrentTheme == SystemTheme.Dark) ? new SystemColors - { - Background = new SKColor((byte)30, (byte)30, (byte)30), - Surface = new SKColor((byte)45, (byte)45, (byte)45), - Primary = AccentColor, - OnPrimary = SKColors.White, - Text = new SKColor((byte)240, (byte)240, (byte)240), - TextSecondary = new SKColor((byte)160, (byte)160, (byte)160), - Border = new SKColor((byte)64, (byte)64, (byte)64), - Divider = new SKColor((byte)58, (byte)58, (byte)58), - Error = new SKColor((byte)207, (byte)102, (byte)121), - Success = new SKColor((byte)129, (byte)201, (byte)149) - } : new SystemColors - { - Background = new SKColor((byte)250, (byte)250, (byte)250), - Surface = SKColors.White, - Primary = AccentColor, - OnPrimary = SKColors.White, - Text = new SKColor((byte)33, (byte)33, (byte)33), - TextSecondary = new SKColor((byte)117, (byte)117, (byte)117), - Border = new SKColor((byte)224, (byte)224, (byte)224), - Divider = new SKColor((byte)238, (byte)238, (byte)238), - Error = new SKColor((byte)176, (byte)0, (byte)32), - Success = new SKColor((byte)46, (byte)125, (byte)50) - }); - } - - private void SetupWatcher() - { - try - { - string path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config"); - if (Directory.Exists(path)) - { - _settingsWatcher = new FileSystemWatcher(path) - { - NotifyFilter = NotifyFilters.LastWrite, - IncludeSubdirectories = true, - EnableRaisingEvents = true - }; - _settingsWatcher.Changed += OnSettingsChanged; - } - } - catch - { - } - } - - private void OnSettingsChanged(object sender, FileSystemEventArgs e) - { - string? name = e.Name; - if (name == null || !name.Contains("kdeglobals")) - { - string? name2 = e.Name; - if (name2 == null || !name2.Contains("gtk")) - { - string? name3 = e.Name; - if (name3 == null || !name3.Contains("settings")) - { - return; - } - } - } - Task.Delay(500).ContinueWith(delegate - { - SystemTheme currentTheme = CurrentTheme; - DetectTheme(); - UpdateColors(); - if (currentTheme != CurrentTheme) - { - this.ThemeChanged?.Invoke(this, new ThemeChangedEventArgs(CurrentTheme)); - } - }); - } - - private string RunCommand(string command, string arguments) - { - try - { - using Process process = new Process - { - StartInfo = new ProcessStartInfo - { - FileName = command, - Arguments = arguments, - RedirectStandardOutput = true, - UseShellExecute = false, - CreateNoWindow = true - } - }; - process.Start(); - string result = process.StandardOutput.ReadToEnd(); - process.WaitForExit(1000); - return result; - } - catch - { - return ""; - } - } - - public void RefreshTheme() - { - SystemTheme currentTheme = CurrentTheme; - DetectTheme(); - UpdateColors(); - if (currentTheme != CurrentTheme) - { - this.ThemeChanged?.Invoke(this, new ThemeChangedEventArgs(CurrentTheme)); - } - } -} diff --git a/Services/SystemTrayService.cs b/Services/SystemTrayService.cs index 13bc46d..cc00838 100644 --- a/Services/SystemTrayService.cs +++ b/Services/SystemTrayService.cs @@ -1,263 +1,282 @@ -using System; -using System.Collections.Generic; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Threading.Tasks; namespace Microsoft.Maui.Platform.Linux.Services; +/// +/// Linux system tray service using various backends. +/// Supports yad, zenity, or native D-Bus StatusNotifierItem. +/// public class SystemTrayService : IDisposable { - private Process? _trayProcess; + private Process? _trayProcess; + private readonly string _appName; + private string? _iconPath; + private string? _tooltip; + private readonly List _menuItems = new(); + private bool _isVisible; + private bool _disposed; - private readonly string _appName; + public event EventHandler? Clicked; + public event EventHandler? MenuItemClicked; - private string? _iconPath; + public SystemTrayService(string appName) + { + _appName = appName; + } - private string? _tooltip; + /// + /// Gets or sets the tray icon path. + /// + public string? IconPath + { + get => _iconPath; + set + { + _iconPath = value; + if (_isVisible) UpdateTray(); + } + } - private readonly List _menuItems = new List(); + /// + /// Gets or sets the tooltip text. + /// + public string? Tooltip + { + get => _tooltip; + set + { + _tooltip = value; + if (_isVisible) UpdateTray(); + } + } - private bool _isVisible; + /// + /// Gets the menu items. + /// + public IList MenuItems => _menuItems; - private bool _disposed; + /// + /// Shows the system tray icon. + /// + public async Task ShowAsync() + { + if (_isVisible) return; - public string? IconPath - { - get - { - return _iconPath; - } - set - { - _iconPath = value; - if (_isVisible) - { - UpdateTray(); - } - } - } + // Try yad first (most feature-complete) + if (await TryYadTray()) + { + _isVisible = true; + return; + } - public string? Tooltip - { - get - { - return _tooltip; - } - set - { - _tooltip = value; - if (_isVisible) - { - UpdateTray(); - } - } - } + // Fall back to a simple approach + _isVisible = true; + } - public IList MenuItems => _menuItems; + /// + /// Hides the system tray icon. + /// + public void Hide() + { + if (!_isVisible) return; - public event EventHandler? Clicked; + _trayProcess?.Kill(); + _trayProcess?.Dispose(); + _trayProcess = null; + _isVisible = false; + } - public event EventHandler? MenuItemClicked; + /// + /// Updates the tray icon and menu. + /// + public void UpdateTray() + { + if (!_isVisible) return; - public SystemTrayService(string appName) - { - _appName = appName; - } + // Restart tray with new settings + Hide(); + _ = ShowAsync(); + } - public async Task ShowAsync() - { - if (!_isVisible) - { - await TryYadTray(); - _isVisible = true; - } - } + private async Task TryYadTray() + { + try + { + var args = BuildYadArgs(); - public void Hide() - { - if (_isVisible) - { - _trayProcess?.Kill(); - _trayProcess?.Dispose(); - _trayProcess = null; - _isVisible = false; - } - } + var startInfo = new ProcessStartInfo + { + FileName = "yad", + Arguments = args, + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; - public void UpdateTray() - { - if (_isVisible) - { - Hide(); - ShowAsync(); - } - } + _trayProcess = Process.Start(startInfo); + if (_trayProcess == null) return false; - private async Task TryYadTray() - { - try - { - string arguments = BuildYadArgs(); - ProcessStartInfo startInfo = new ProcessStartInfo - { - FileName = "yad", - Arguments = arguments, - UseShellExecute = false, - RedirectStandardOutput = true, - RedirectStandardError = true, - CreateNoWindow = true - }; - _trayProcess = Process.Start(startInfo); - if (_trayProcess == null) - { - return false; - } - Task.Run(async delegate - { - try - { - while (!_trayProcess.HasExited) - { - string text = await _trayProcess.StandardOutput.ReadLineAsync(); - if (!string.IsNullOrEmpty(text)) - { - HandleTrayOutput(text); - } - } - } - catch - { - } - }); - return true; - } - catch - { - return false; - } - } + // Start reading output for menu clicks + _ = Task.Run(async () => + { + try + { + while (!_trayProcess.HasExited) + { + var line = await _trayProcess.StandardOutput.ReadLineAsync(); + if (!string.IsNullOrEmpty(line)) + { + HandleTrayOutput(line); + } + } + } + catch { } + }); - private string BuildYadArgs() - { - List list = new List { "--notification", "--listen" }; - if (!string.IsNullOrEmpty(_iconPath) && File.Exists(_iconPath)) - { - list.Add("--image=\"" + _iconPath + "\""); - } - else - { - list.Add("--image=application-x-executable"); - } - if (!string.IsNullOrEmpty(_tooltip)) - { - list.Add("--text=\"" + EscapeArg(_tooltip) + "\""); - } - if (_menuItems.Count > 0) - { - string text = string.Join("!", _menuItems.Select(delegate(TrayMenuItem m) - { - object obj; - if (!m.IsSeparator) - { - obj = EscapeArg(m.Text); - if (obj == null) - { - return ""; - } - } - else - { - obj = "---"; - } - return (string)obj; - })); - list.Add("--menu=\"" + text + "\""); - } - list.Add("--command=\"echo clicked\""); - return string.Join(" ", list); - } + return true; + } + catch + { + return false; + } + } - private void HandleTrayOutput(string output) - { - if (output == "clicked") - { - this.Clicked?.Invoke(this, EventArgs.Empty); - return; - } - TrayMenuItem trayMenuItem = _menuItems.FirstOrDefault((TrayMenuItem m) => m.Text == output); - if (trayMenuItem != null) - { - trayMenuItem.Action?.Invoke(); - this.MenuItemClicked?.Invoke(this, output); - } - } + private string BuildYadArgs() + { + var args = new List + { + "--notification", + "--listen" + }; - public void AddMenuItem(string text, Action? action = null) - { - _menuItems.Add(new TrayMenuItem - { - Text = text, - Action = action - }); - } + if (!string.IsNullOrEmpty(_iconPath) && File.Exists(_iconPath)) + { + args.Add($"--image=\"{_iconPath}\""); + } + else + { + args.Add("--image=application-x-executable"); + } - public void AddSeparator() - { - _menuItems.Add(new TrayMenuItem - { - IsSeparator = true - }); - } + if (!string.IsNullOrEmpty(_tooltip)) + { + args.Add($"--text=\"{EscapeArg(_tooltip)}\""); + } - public void ClearMenuItems() - { - _menuItems.Clear(); - } + // Build menu + if (_menuItems.Count > 0) + { + var menuStr = string.Join("!", _menuItems.Select(m => + m.IsSeparator ? "---" : $"{EscapeArg(m.Text)}")); + args.Add($"--menu=\"{menuStr}\""); + } - public static bool IsAvailable() - { - try - { - using Process process = Process.Start(new ProcessStartInfo - { - FileName = "which", - Arguments = "yad", - UseShellExecute = false, - RedirectStandardOutput = true, - CreateNoWindow = true - }); - if (process == null) - { - return false; - } - process.WaitForExit(); - return process.ExitCode == 0; - } - catch - { - return false; - } - } + args.Add("--command=\"echo clicked\""); - private static string EscapeArg(string arg) - { - return arg?.Replace("\\", "\\\\").Replace("\"", "\\\"").Replace("!", "\\!") ?? ""; - } + return string.Join(" ", args); + } - public void Dispose() - { - if (!_disposed) - { - _disposed = true; - Hide(); - GC.SuppressFinalize(this); - } - } + private void HandleTrayOutput(string output) + { + if (output == "clicked") + { + Clicked?.Invoke(this, EventArgs.Empty); + } + else + { + // Menu item clicked + var menuItem = _menuItems.FirstOrDefault(m => m.Text == output); + if (menuItem != null) + { + menuItem.Action?.Invoke(); + MenuItemClicked?.Invoke(this, output); + } + } + } - ~SystemTrayService() - { - Dispose(); - } + /// + /// Adds a menu item to the tray context menu. + /// + public void AddMenuItem(string text, Action? action = null) + { + _menuItems.Add(new TrayMenuItem { Text = text, Action = action }); + } + + /// + /// Adds a separator to the tray context menu. + /// + public void AddSeparator() + { + _menuItems.Add(new TrayMenuItem { IsSeparator = true }); + } + + /// + /// Clears all menu items. + /// + public void ClearMenuItems() + { + _menuItems.Clear(); + } + + /// + /// Checks if system tray is available on this system. + /// + public static bool IsAvailable() + { + try + { + var startInfo = new ProcessStartInfo + { + FileName = "which", + Arguments = "yad", + UseShellExecute = false, + RedirectStandardOutput = true, + CreateNoWindow = true + }; + + using var process = Process.Start(startInfo); + if (process == null) return false; + + process.WaitForExit(); + return process.ExitCode == 0; + } + catch + { + return false; + } + } + + private static string EscapeArg(string arg) + { + return arg?.Replace("\\", "\\\\").Replace("\"", "\\\"").Replace("!", "\\!") ?? ""; + } + + public void Dispose() + { + if (_disposed) return; + _disposed = true; + Hide(); + GC.SuppressFinalize(this); + } + + ~SystemTrayService() + { + Dispose(); + } +} + +/// +/// Represents a tray menu item. +/// +public class TrayMenuItem +{ + public string Text { get; set; } = ""; + public Action? Action { get; set; } + public bool IsSeparator { get; set; } + public bool IsEnabled { get; set; } = true; + public string? IconPath { get; set; } } diff --git a/Services/TextCommittedEventArgs.cs b/Services/TextCommittedEventArgs.cs deleted file mode 100644 index 358eeef..0000000 --- a/Services/TextCommittedEventArgs.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; - -namespace Microsoft.Maui.Platform.Linux.Services; - -public class TextCommittedEventArgs : EventArgs -{ - public string Text { get; } - - public TextCommittedEventArgs(string text) - { - Text = text; - } -} diff --git a/Services/TextRun.cs b/Services/TextRun.cs deleted file mode 100644 index cae4cd8..0000000 --- a/Services/TextRun.cs +++ /dev/null @@ -1,19 +0,0 @@ -using SkiaSharp; - -namespace Microsoft.Maui.Platform.Linux.Services; - -public class TextRun -{ - public string Text { get; } - - public SKTypeface Typeface { get; } - - public int StartIndex { get; } - - public TextRun(string text, SKTypeface typeface, int startIndex) - { - Text = text; - Typeface = typeface; - StartIndex = startIndex; - } -} diff --git a/Services/ThemeChangedEventArgs.cs b/Services/ThemeChangedEventArgs.cs deleted file mode 100644 index 8eac3e5..0000000 --- a/Services/ThemeChangedEventArgs.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; - -namespace Microsoft.Maui.Platform.Linux.Services; - -public class ThemeChangedEventArgs : EventArgs -{ - public SystemTheme NewTheme { get; } - - public ThemeChangedEventArgs(SystemTheme newTheme) - { - NewTheme = newTheme; - } -} diff --git a/Services/TrayMenuItem.cs b/Services/TrayMenuItem.cs deleted file mode 100644 index d3608bb..0000000 --- a/Services/TrayMenuItem.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; - -namespace Microsoft.Maui.Platform.Linux.Services; - -public class TrayMenuItem -{ - public string Text { get; set; } = ""; - - public Action? Action { get; set; } - - public bool IsSeparator { get; set; } - - public bool IsEnabled { get; set; } = true; - - public string? IconPath { get; set; } -} diff --git a/Services/VersionTrackingService.cs b/Services/VersionTrackingService.cs index 8b380d6..7459bc0 100644 --- a/Services/VersionTrackingService.cs +++ b/Services/VersionTrackingService.cs @@ -1,250 +1,251 @@ -using System; -using System.Collections.Generic; -using System.IO; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using System.Reflection; using System.Text.Json; using Microsoft.Maui.ApplicationModel; namespace Microsoft.Maui.Platform.Linux.Services; +/// +/// Linux version tracking implementation. +/// public class VersionTrackingService : IVersionTracking { - private class VersionTrackingData - { - public string? CurrentVersion { get; set; } + private const string VersionTrackingFile = ".maui-version-tracking.json"; + private readonly string _trackingFilePath; + private VersionTrackingData _data; + private bool _isInitialized; - public string? CurrentBuild { get; set; } + public VersionTrackingService() + { + _trackingFilePath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + VersionTrackingFile); + _data = new VersionTrackingData(); + } - public string? PreviousVersion { get; set; } + private void EnsureInitialized() + { + if (_isInitialized) return; + _isInitialized = true; - public string? PreviousBuild { get; set; } + LoadTrackingData(); + UpdateTrackingData(); + } - public string? FirstInstalledVersion { get; set; } + private void LoadTrackingData() + { + try + { + if (File.Exists(_trackingFilePath)) + { + var json = File.ReadAllText(_trackingFilePath); + _data = JsonSerializer.Deserialize(json) ?? new VersionTrackingData(); + } + } + catch + { + _data = new VersionTrackingData(); + } + } - public string? FirstInstalledBuild { get; set; } + private void UpdateTrackingData() + { + var currentVersion = CurrentVersion; + var currentBuild = CurrentBuild; - public List VersionHistory { get; set; } = new List(); + // Check if this is a new version + if (_data.PreviousVersion != currentVersion || _data.PreviousBuild != currentBuild) + { + // Store previous version info + if (!string.IsNullOrEmpty(_data.CurrentVersion)) + { + _data.PreviousVersion = _data.CurrentVersion; + _data.PreviousBuild = _data.CurrentBuild; + } - public List BuildHistory { get; set; } = new List(); + _data.CurrentVersion = currentVersion; + _data.CurrentBuild = currentBuild; - public bool IsFirstLaunchEver { get; set; } + // Add to version history + if (!_data.VersionHistory.Contains(currentVersion)) + { + _data.VersionHistory.Add(currentVersion); + } - public bool IsFirstLaunchForCurrentVersion { get; set; } + // Add to build history + if (!_data.BuildHistory.Contains(currentBuild)) + { + _data.BuildHistory.Add(currentBuild); + } + } - public bool IsFirstLaunchForCurrentBuild { get; set; } - } + // Track first launch + if (_data.FirstInstalledVersion == null) + { + _data.FirstInstalledVersion = currentVersion; + _data.FirstInstalledBuild = currentBuild; + _data.IsFirstLaunchEver = true; + } + else + { + _data.IsFirstLaunchEver = false; + } - private const string VersionTrackingFile = ".maui-version-tracking.json"; + // Check if first launch for current version + _data.IsFirstLaunchForCurrentVersion = _data.PreviousVersion != currentVersion; + _data.IsFirstLaunchForCurrentBuild = _data.PreviousBuild != currentBuild; - private readonly string _trackingFilePath; + SaveTrackingData(); + } - private VersionTrackingData _data; + private void SaveTrackingData() + { + try + { + var directory = Path.GetDirectoryName(_trackingFilePath); + if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } - private bool _isInitialized; + var json = JsonSerializer.Serialize(_data, new JsonSerializerOptions { WriteIndented = true }); + File.WriteAllText(_trackingFilePath, json); + } + catch + { + // Silently fail if we can't save + } + } - public bool IsFirstLaunchEver - { - get - { - EnsureInitialized(); - return _data.IsFirstLaunchEver; - } - } + public bool IsFirstLaunchEver + { + get + { + EnsureInitialized(); + return _data.IsFirstLaunchEver; + } + } - public bool IsFirstLaunchForCurrentVersion - { - get - { - EnsureInitialized(); - return _data.IsFirstLaunchForCurrentVersion; - } - } + public bool IsFirstLaunchForCurrentVersion + { + get + { + EnsureInitialized(); + return _data.IsFirstLaunchForCurrentVersion; + } + } - public bool IsFirstLaunchForCurrentBuild - { - get - { - EnsureInitialized(); - return _data.IsFirstLaunchForCurrentBuild; - } - } + public bool IsFirstLaunchForCurrentBuild + { + get + { + EnsureInitialized(); + return _data.IsFirstLaunchForCurrentBuild; + } + } - public string CurrentVersion => GetAssemblyVersion(); + public string CurrentVersion => GetAssemblyVersion(); + public string CurrentBuild => GetAssemblyBuild(); - public string CurrentBuild => GetAssemblyBuild(); + public string? PreviousVersion + { + get + { + EnsureInitialized(); + return _data.PreviousVersion; + } + } - public string? PreviousVersion - { - get - { - EnsureInitialized(); - return _data.PreviousVersion; - } - } + public string? PreviousBuild + { + get + { + EnsureInitialized(); + return _data.PreviousBuild; + } + } - public string? PreviousBuild - { - get - { - EnsureInitialized(); - return _data.PreviousBuild; - } - } + public string? FirstInstalledVersion + { + get + { + EnsureInitialized(); + return _data.FirstInstalledVersion; + } + } - public string? FirstInstalledVersion - { - get - { - EnsureInitialized(); - return _data.FirstInstalledVersion; - } - } + public string? FirstInstalledBuild + { + get + { + EnsureInitialized(); + return _data.FirstInstalledBuild; + } + } - public string? FirstInstalledBuild - { - get - { - EnsureInitialized(); - return _data.FirstInstalledBuild; - } - } + public IReadOnlyList VersionHistory + { + get + { + EnsureInitialized(); + return _data.VersionHistory.AsReadOnly(); + } + } - public IReadOnlyList VersionHistory - { - get - { - EnsureInitialized(); - return _data.VersionHistory.AsReadOnly(); - } - } + public IReadOnlyList BuildHistory + { + get + { + EnsureInitialized(); + return _data.BuildHistory.AsReadOnly(); + } + } - public IReadOnlyList BuildHistory - { - get - { - EnsureInitialized(); - return _data.BuildHistory.AsReadOnly(); - } - } + public bool IsFirstLaunchForVersion(string version) + { + EnsureInitialized(); + return !_data.VersionHistory.Contains(version); + } - public VersionTrackingService() - { - _trackingFilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), ".maui-version-tracking.json"); - _data = new VersionTrackingData(); - } + public bool IsFirstLaunchForBuild(string build) + { + EnsureInitialized(); + return !_data.BuildHistory.Contains(build); + } - private void EnsureInitialized() - { - if (!_isInitialized) - { - _isInitialized = true; - LoadTrackingData(); - UpdateTrackingData(); - } - } + public void Track() + { + EnsureInitialized(); + } - private void LoadTrackingData() - { - try - { - if (File.Exists(_trackingFilePath)) - { - string json = File.ReadAllText(_trackingFilePath); - _data = JsonSerializer.Deserialize(json) ?? new VersionTrackingData(); - } - } - catch - { - _data = new VersionTrackingData(); - } - } + private static string GetAssemblyVersion() + { + var assembly = Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly(); + var version = assembly.GetName().Version; + return version != null ? $"{version.Major}.{version.Minor}.{version.Build}" : "1.0.0"; + } - private void UpdateTrackingData() - { - string currentVersion = CurrentVersion; - string currentBuild = CurrentBuild; - if (_data.PreviousVersion != currentVersion || _data.PreviousBuild != currentBuild) - { - if (!string.IsNullOrEmpty(_data.CurrentVersion)) - { - _data.PreviousVersion = _data.CurrentVersion; - _data.PreviousBuild = _data.CurrentBuild; - } - _data.CurrentVersion = currentVersion; - _data.CurrentBuild = currentBuild; - if (!_data.VersionHistory.Contains(currentVersion)) - { - _data.VersionHistory.Add(currentVersion); - } - if (!_data.BuildHistory.Contains(currentBuild)) - { - _data.BuildHistory.Add(currentBuild); - } - } - if (_data.FirstInstalledVersion == null) - { - _data.FirstInstalledVersion = currentVersion; - _data.FirstInstalledBuild = currentBuild; - _data.IsFirstLaunchEver = true; - } - else - { - _data.IsFirstLaunchEver = false; - } - _data.IsFirstLaunchForCurrentVersion = _data.PreviousVersion != currentVersion; - _data.IsFirstLaunchForCurrentBuild = _data.PreviousBuild != currentBuild; - SaveTrackingData(); - } + private static string GetAssemblyBuild() + { + var assembly = Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly(); + var version = assembly.GetName().Version; + return version?.Revision.ToString() ?? "0"; + } - private void SaveTrackingData() - { - try - { - string directoryName = Path.GetDirectoryName(_trackingFilePath); - if (!string.IsNullOrEmpty(directoryName) && !Directory.Exists(directoryName)) - { - Directory.CreateDirectory(directoryName); - } - string contents = JsonSerializer.Serialize(_data, new JsonSerializerOptions - { - WriteIndented = true - }); - File.WriteAllText(_trackingFilePath, contents); - } - catch - { - } - } - - public bool IsFirstLaunchForVersion(string version) - { - EnsureInitialized(); - return !_data.VersionHistory.Contains(version); - } - - public bool IsFirstLaunchForBuild(string build) - { - EnsureInitialized(); - return !_data.BuildHistory.Contains(build); - } - - public void Track() - { - EnsureInitialized(); - } - - private static string GetAssemblyVersion() - { - Version version = (Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly()).GetName().Version; - if (!(version != null)) - { - return "1.0.0"; - } - return $"{version.Major}.{version.Minor}.{version.Build}"; - } - - private static string GetAssemblyBuild() - { - return (Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly()).GetName().Version?.Revision.ToString() ?? "0"; - } + private class VersionTrackingData + { + public string? CurrentVersion { get; set; } + public string? CurrentBuild { get; set; } + public string? PreviousVersion { get; set; } + public string? PreviousBuild { get; set; } + public string? FirstInstalledVersion { get; set; } + public string? FirstInstalledBuild { get; set; } + public List VersionHistory { get; set; } = new(); + public List BuildHistory { get; set; } = new(); + public bool IsFirstLaunchEver { get; set; } + public bool IsFirstLaunchForCurrentVersion { get; set; } + public bool IsFirstLaunchForCurrentBuild { get; set; } + } } diff --git a/Services/VideoAccelerationApi.cs b/Services/VideoAccelerationApi.cs deleted file mode 100644 index 6f4d388..0000000 --- a/Services/VideoAccelerationApi.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Microsoft.Maui.Platform.Linux.Services; - -public enum VideoAccelerationApi -{ - Auto, - VaApi, - Vdpau, - Software -} diff --git a/Services/VideoFrame.cs b/Services/VideoFrame.cs deleted file mode 100644 index 13e3070..0000000 --- a/Services/VideoFrame.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; - -namespace Microsoft.Maui.Platform.Linux.Services; - -public class VideoFrame : IDisposable -{ - private bool _disposed; - - private Action? _releaseCallback; - - public int Width { get; init; } - - public int Height { get; init; } - - public IntPtr DataY { get; init; } - - public IntPtr DataU { get; init; } - - public IntPtr DataV { get; init; } - - public int StrideY { get; init; } - - public int StrideU { get; init; } - - public int StrideV { get; init; } - - public long Timestamp { get; init; } - - public bool IsKeyFrame { get; init; } - - internal void SetReleaseCallback(Action callback) - { - _releaseCallback = callback; - } - - public void Dispose() - { - if (!_disposed) - { - _releaseCallback?.Invoke(); - _disposed = true; - } - } -} diff --git a/Services/VideoProfile.cs b/Services/VideoProfile.cs deleted file mode 100644 index 8a8a7fe..0000000 --- a/Services/VideoProfile.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Microsoft.Maui.Platform.Linux.Services; - -public enum VideoProfile -{ - H264Baseline, - H264Main, - H264High, - H265Main, - H265Main10, - Vp8, - Vp9Profile0, - Vp9Profile2, - Av1Main -} diff --git a/Services/VirtualizationExtensions.cs b/Services/VirtualizationExtensions.cs deleted file mode 100644 index 467d04c..0000000 --- a/Services/VirtualizationExtensions.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System; - -namespace Microsoft.Maui.Platform.Linux.Services; - -public static class VirtualizationExtensions -{ - public static (int first, int last) CalculateVisibleRange(float scrollOffset, float viewportHeight, float itemHeight, float itemSpacing, int totalItems) - { - if (totalItems == 0) - { - return (first: -1, last: -1); - } - float num = itemHeight + itemSpacing; - int item = Math.Max(0, (int)(scrollOffset / num)); - int item2 = Math.Min(totalItems - 1, (int)((scrollOffset + viewportHeight) / num) + 1); - return (first: item, last: item2); - } - - public static (int first, int last) CalculateVisibleRangeVariable(float scrollOffset, float viewportHeight, Func getItemHeight, float itemSpacing, int totalItems) - { - if (totalItems == 0) - { - return (first: -1, last: -1); - } - int num = 0; - float num2 = 0f; - for (int i = 0; i < totalItems; i++) - { - float num3 = getItemHeight(i); - if (num2 + num3 > scrollOffset) - { - num = i; - break; - } - num2 += num3 + itemSpacing; - } - int item = num; - float num4 = scrollOffset + viewportHeight; - for (int j = num; j < totalItems; j++) - { - float num5 = getItemHeight(j); - if (num2 > num4) - { - break; - } - item = j; - num2 += num5 + itemSpacing; - } - return (first: num, last: item); - } - - public static (int firstRow, int lastRow) CalculateVisibleGridRange(float scrollOffset, float viewportHeight, float rowHeight, float rowSpacing, int totalRows) - { - if (totalRows == 0) - { - return (firstRow: -1, lastRow: -1); - } - float num = rowHeight + rowSpacing; - int item = Math.Max(0, (int)(scrollOffset / num)); - int item2 = Math.Min(totalRows - 1, (int)((scrollOffset + viewportHeight) / num) + 1); - return (firstRow: item, lastRow: item2); - } -} diff --git a/Services/VirtualizationManager.cs b/Services/VirtualizationManager.cs deleted file mode 100644 index 504c184..0000000 --- a/Services/VirtualizationManager.cs +++ /dev/null @@ -1,135 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Microsoft.Maui.Platform.Linux.Services; - -public class VirtualizationManager where T : SkiaView -{ - private readonly Dictionary _activeViews = new Dictionary(); - - private readonly Queue _recyclePool = new Queue(); - - private readonly Func _viewFactory; - - private readonly Action? _viewRecycler; - - private readonly int _maxPoolSize; - - private int _firstVisibleIndex = -1; - - private int _lastVisibleIndex = -1; - - public int ActiveViewCount => _activeViews.Count; - - public int PooledViewCount => _recyclePool.Count; - - public (int First, int Last) VisibleRange => (First: _firstVisibleIndex, Last: _lastVisibleIndex); - - public VirtualizationManager(Func viewFactory, Action? viewRecycler = null, int maxPoolSize = 20) - { - _viewFactory = viewFactory ?? throw new ArgumentNullException("viewFactory"); - _viewRecycler = viewRecycler; - _maxPoolSize = maxPoolSize; - } - - public void UpdateVisibleRange(int firstVisible, int lastVisible) - { - if (firstVisible == _firstVisibleIndex && lastVisible == _lastVisibleIndex) - { - return; - } - List list = new List(); - foreach (KeyValuePair activeView in _activeViews) - { - if (activeView.Key < firstVisible || activeView.Key > lastVisible) - { - list.Add(activeView.Key); - } - } - foreach (int item in list) - { - RecycleView(item); - } - _firstVisibleIndex = firstVisible; - _lastVisibleIndex = lastVisible; - } - - public T GetOrCreateView(int index, Action bindData) - { - if (_activeViews.TryGetValue(index, out var value)) - { - return value; - } - T val = ((_recyclePool.Count <= 0) ? _viewFactory() : _recyclePool.Dequeue()); - bindData(val); - _activeViews[index] = val; - return val; - } - - public T? GetActiveView(int index) - { - if (!_activeViews.TryGetValue(index, out var value)) - { - return null; - } - return value; - } - - private void RecycleView(int index) - { - if (_activeViews.TryGetValue(index, out var value)) - { - _activeViews.Remove(index); - _viewRecycler?.Invoke(value); - if (_recyclePool.Count < _maxPoolSize) - { - _recyclePool.Enqueue(value); - } - else - { - value.Dispose(); - } - } - } - - public void Clear() - { - foreach (T value in _activeViews.Values) - { - value.Dispose(); - } - _activeViews.Clear(); - while (_recyclePool.Count > 0) - { - _recyclePool.Dequeue().Dispose(); - } - _firstVisibleIndex = -1; - _lastVisibleIndex = -1; - } - - public void RemoveItem(int index) - { - RecycleView(index); - foreach (KeyValuePair item in (from kvp in _activeViews - where kvp.Key > index - orderby kvp.Key - select kvp).ToList()) - { - _activeViews.Remove(item.Key); - _activeViews[item.Key - 1] = item.Value; - } - } - - public void InsertItem(int index) - { - foreach (KeyValuePair item in (from kvp in _activeViews - where kvp.Key >= index - orderby kvp.Key descending - select kvp).ToList()) - { - _activeViews.Remove(item.Key); - _activeViews[item.Key + 1] = item.Value; - } - } -} diff --git a/Services/WaylandDisplayWindow.cs b/Services/WaylandDisplayWindow.cs deleted file mode 100644 index 891d6c1..0000000 --- a/Services/WaylandDisplayWindow.cs +++ /dev/null @@ -1,124 +0,0 @@ -using System; -using Microsoft.Maui.Platform.Linux.Window; - -namespace Microsoft.Maui.Platform.Linux.Services; - -public class WaylandDisplayWindow : IDisplayWindow, IDisposable -{ - private readonly WaylandWindow _window; - - public int Width => _window.Width; - - public int Height => _window.Height; - - public bool IsRunning => _window.IsRunning; - - public IntPtr PixelData => _window.PixelData; - - public int Stride => _window.Stride; - - public event EventHandler? KeyDown; - - public event EventHandler? KeyUp; - - public event EventHandler? TextInput; - - public event EventHandler? PointerMoved; - - public event EventHandler? PointerPressed; - - public event EventHandler? PointerReleased; - - public event EventHandler? Scroll; - - public event EventHandler? Exposed; - - public event EventHandler<(int Width, int Height)>? Resized; - - public event EventHandler? CloseRequested; - - public WaylandDisplayWindow(string title, int width, int height) - { - _window = new WaylandWindow(title, width, height); - _window.KeyDown += delegate(object? s, KeyEventArgs e) - { - this.KeyDown?.Invoke(this, e); - }; - _window.KeyUp += delegate(object? s, KeyEventArgs e) - { - this.KeyUp?.Invoke(this, e); - }; - _window.TextInput += delegate(object? s, TextInputEventArgs e) - { - this.TextInput?.Invoke(this, e); - }; - _window.PointerMoved += delegate(object? s, PointerEventArgs e) - { - this.PointerMoved?.Invoke(this, e); - }; - _window.PointerPressed += delegate(object? s, PointerEventArgs e) - { - this.PointerPressed?.Invoke(this, e); - }; - _window.PointerReleased += delegate(object? s, PointerEventArgs e) - { - this.PointerReleased?.Invoke(this, e); - }; - _window.Scroll += delegate(object? s, ScrollEventArgs e) - { - this.Scroll?.Invoke(this, e); - }; - _window.Exposed += delegate(object? s, EventArgs e) - { - this.Exposed?.Invoke(this, e); - }; - _window.Resized += delegate(object? s, (int Width, int Height) e) - { - this.Resized?.Invoke(this, e); - }; - _window.CloseRequested += delegate(object? s, EventArgs e) - { - this.CloseRequested?.Invoke(this, e); - }; - } - - public void Show() - { - _window.Show(); - } - - public void Hide() - { - _window.Hide(); - } - - public void SetTitle(string title) - { - _window.SetTitle(title); - } - - public void Resize(int width, int height) - { - _window.Resize(width, height); - } - - public void ProcessEvents() - { - _window.ProcessEvents(); - } - - public void Stop() - { - _window.Stop(); - } - - public void CommitFrame() - { - _window.CommitFrame(); - } - - public void Dispose() - { - _window.Dispose(); - } -} diff --git a/Services/X11DisplayWindow.cs b/Services/X11DisplayWindow.cs deleted file mode 100644 index 9976189..0000000 --- a/Services/X11DisplayWindow.cs +++ /dev/null @@ -1,115 +0,0 @@ -using System; -using Microsoft.Maui.Platform.Linux.Window; - -namespace Microsoft.Maui.Platform.Linux.Services; - -public class X11DisplayWindow : IDisplayWindow, IDisposable -{ - private readonly X11Window _window; - - public int Width => _window.Width; - - public int Height => _window.Height; - - public bool IsRunning => _window.IsRunning; - - public event EventHandler? KeyDown; - - public event EventHandler? KeyUp; - - public event EventHandler? TextInput; - - public event EventHandler? PointerMoved; - - public event EventHandler? PointerPressed; - - public event EventHandler? PointerReleased; - - public event EventHandler? Scroll; - - public event EventHandler? Exposed; - - public event EventHandler<(int Width, int Height)>? Resized; - - public event EventHandler? CloseRequested; - - public X11DisplayWindow(string title, int width, int height) - { - _window = new X11Window(title, width, height); - _window.KeyDown += delegate(object? s, KeyEventArgs e) - { - this.KeyDown?.Invoke(this, e); - }; - _window.KeyUp += delegate(object? s, KeyEventArgs e) - { - this.KeyUp?.Invoke(this, e); - }; - _window.TextInput += delegate(object? s, TextInputEventArgs e) - { - this.TextInput?.Invoke(this, e); - }; - _window.PointerMoved += delegate(object? s, PointerEventArgs e) - { - this.PointerMoved?.Invoke(this, e); - }; - _window.PointerPressed += delegate(object? s, PointerEventArgs e) - { - this.PointerPressed?.Invoke(this, e); - }; - _window.PointerReleased += delegate(object? s, PointerEventArgs e) - { - this.PointerReleased?.Invoke(this, e); - }; - _window.Scroll += delegate(object? s, ScrollEventArgs e) - { - this.Scroll?.Invoke(this, e); - }; - _window.Exposed += delegate(object? s, EventArgs e) - { - this.Exposed?.Invoke(this, e); - }; - _window.Resized += delegate(object? s, (int Width, int Height) e) - { - this.Resized?.Invoke(this, e); - }; - _window.CloseRequested += delegate(object? s, EventArgs e) - { - this.CloseRequested?.Invoke(this, e); - }; - } - - public void Show() - { - _window.Show(); - } - - public void Hide() - { - _window.Hide(); - } - - public void SetTitle(string title) - { - _window.SetTitle(title); - } - - public void Resize(int width, int height) - { - _window.Resize(width, height); - } - - public void ProcessEvents() - { - _window.ProcessEvents(); - } - - public void Stop() - { - _window.Stop(); - } - - public void Dispose() - { - _window.Dispose(); - } -} diff --git a/Services/X11InputMethodService.cs b/Services/X11InputMethodService.cs index 24c6a94..5cc3bbb 100644 --- a/Services/X11InputMethodService.cs +++ b/Services/X11InputMethodService.cs @@ -1,399 +1,394 @@ -using System; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using System.Runtime.InteropServices; using System.Text; namespace Microsoft.Maui.Platform.Linux.Services; +/// +/// X11 Input Method service using XIM protocol. +/// Provides IME support for CJK and other complex input methods. +/// public class X11InputMethodService : IInputMethodService, IDisposable { - private delegate int XIMProc(IntPtr xic, IntPtr clientData, IntPtr callData); + private nint _display; + private nint _window; + private nint _xim; + private nint _xic; + private IInputContext? _currentContext; + private string _preEditText = string.Empty; + private int _preEditCursorPosition; + private bool _isActive; + private bool _disposed; + + // XIM callback delegates (prevent GC) + private XIMProc? _preeditStartCallback; + private XIMProc? _preeditDoneCallback; + private XIMProc? _preeditDrawCallback; + private XIMProc? _preeditCaretCallback; + private XIMProc? _commitCallback; + + public bool IsActive => _isActive; + public string PreEditText => _preEditText; + public int PreEditCursorPosition => _preEditCursorPosition; + + public event EventHandler? TextCommitted; + public event EventHandler? PreEditChanged; + public event EventHandler? PreEditEnded; + + public void Initialize(nint windowHandle) + { + _window = windowHandle; + + // Get display from X11 interop + _display = XOpenDisplay(IntPtr.Zero); + if (_display == IntPtr.Zero) + { + Console.WriteLine("X11InputMethodService: Failed to open display"); + return; + } + + // Set locale for proper IME operation + if (XSetLocaleModifiers("") == IntPtr.Zero) + { + XSetLocaleModifiers("@im=none"); + } + + // Open input method + _xim = XOpenIM(_display, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero); + if (_xim == IntPtr.Zero) + { + Console.WriteLine("X11InputMethodService: No input method available, trying IBus..."); + TryIBusFallback(); + return; + } + + CreateInputContext(); + } + + private void CreateInputContext() + { + if (_xim == IntPtr.Zero || _window == IntPtr.Zero) return; + + // Create input context with preedit callbacks + var preeditAttr = CreatePreeditAttributes(); + + _xic = XCreateIC(_xim, + XNClientWindow, _window, + XNFocusWindow, _window, + XNInputStyle, XIMPreeditCallbacks | XIMStatusNothing, + XNPreeditAttributes, preeditAttr, + IntPtr.Zero); + + if (preeditAttr != IntPtr.Zero) + { + XFree(preeditAttr); + } + + if (_xic == IntPtr.Zero) + { + // Fallback to simpler input style + _xic = XCreateICSimple(_xim, + XNClientWindow, _window, + XNFocusWindow, _window, + XNInputStyle, XIMPreeditNothing | XIMStatusNothing, + IntPtr.Zero); + } + + if (_xic != IntPtr.Zero) + { + Console.WriteLine("X11InputMethodService: Input context created successfully"); + } + } + + private nint CreatePreeditAttributes() + { + // Set up preedit callbacks for on-the-spot composition + _preeditStartCallback = PreeditStartCallback; + _preeditDoneCallback = PreeditDoneCallback; + _preeditDrawCallback = PreeditDrawCallback; + _preeditCaretCallback = PreeditCaretCallback; + + // Create callback structures + // Note: Actual implementation would marshal XIMCallback structures + return IntPtr.Zero; + } + + private int PreeditStartCallback(nint xic, nint clientData, nint callData) + { + _isActive = true; + _preEditText = string.Empty; + _preEditCursorPosition = 0; + return -1; // No length limit + } + + private int PreeditDoneCallback(nint xic, nint clientData, nint callData) + { + _isActive = false; + _preEditText = string.Empty; + _preEditCursorPosition = 0; + PreEditEnded?.Invoke(this, EventArgs.Empty); + _currentContext?.OnPreEditEnded(); + return 0; + } + + private int PreeditDrawCallback(nint xic, nint clientData, nint callData) + { + // Parse XIMPreeditDrawCallbackStruct + // Update preedit text and cursor position + // This would involve marshaling the callback data structure + + PreEditChanged?.Invoke(this, new PreEditChangedEventArgs(_preEditText, _preEditCursorPosition)); + _currentContext?.OnPreEditChanged(_preEditText, _preEditCursorPosition); + return 0; + } + + private int PreeditCaretCallback(nint xic, nint clientData, nint callData) + { + // Handle caret movement in preedit text + return 0; + } + + private void TryIBusFallback() + { + // Try to connect to IBus via D-Bus + // This provides a more modern IME interface + Console.WriteLine("X11InputMethodService: IBus fallback not yet implemented"); + } + + public void SetFocus(IInputContext? context) + { + _currentContext = context; + + if (_xic != IntPtr.Zero) + { + if (context != null) + { + XSetICFocus(_xic); + } + else + { + XUnsetICFocus(_xic); + } + } + } + + public void SetCursorLocation(int x, int y, int width, int height) + { + if (_xic == IntPtr.Zero) return; + + // Set the spot location for candidate window positioning + var spotLocation = new XPoint { x = (short)x, y = (short)y }; + + var attr = XVaCreateNestedList(0, + XNSpotLocation, ref spotLocation, + IntPtr.Zero); + + if (attr != IntPtr.Zero) + { + XSetICValues(_xic, XNPreeditAttributes, attr, IntPtr.Zero); + XFree(attr); + } + } + + public bool ProcessKeyEvent(uint keyCode, KeyModifiers modifiers, bool isKeyDown) + { + if (_xic == IntPtr.Zero) return false; + + // Convert to X11 key event + var xEvent = new XKeyEvent + { + type = isKeyDown ? KeyPress : KeyRelease, + display = _display, + window = _window, + state = ConvertModifiers(modifiers), + keycode = keyCode + }; + + // Filter through XIM + if (XFilterEvent(ref xEvent, _window)) + { + return true; // Event consumed by IME + } + + // If not filtered and key down, try to get committed text + if (isKeyDown) + { + var buffer = new byte[64]; + var keySym = IntPtr.Zero; + var status = IntPtr.Zero; + + int len = Xutf8LookupString(_xic, ref xEvent, buffer, buffer.Length, ref keySym, ref status); + + if (len > 0) + { + string text = Encoding.UTF8.GetString(buffer, 0, len); + OnTextCommit(text); + return true; + } + } + + return false; + } + + private void OnTextCommit(string text) + { + _preEditText = string.Empty; + _preEditCursorPosition = 0; + + TextCommitted?.Invoke(this, new TextCommittedEventArgs(text)); + _currentContext?.OnTextCommitted(text); + } + + private uint ConvertModifiers(KeyModifiers modifiers) + { + uint state = 0; + if (modifiers.HasFlag(KeyModifiers.Shift)) state |= ShiftMask; + if (modifiers.HasFlag(KeyModifiers.Control)) state |= ControlMask; + if (modifiers.HasFlag(KeyModifiers.Alt)) state |= Mod1Mask; + if (modifiers.HasFlag(KeyModifiers.Super)) state |= Mod4Mask; + if (modifiers.HasFlag(KeyModifiers.CapsLock)) state |= LockMask; + if (modifiers.HasFlag(KeyModifiers.NumLock)) state |= Mod2Mask; + return state; + } + + public void Reset() + { + if (_xic != IntPtr.Zero) + { + XmbResetIC(_xic); + } + + _preEditText = string.Empty; + _preEditCursorPosition = 0; + _isActive = false; + + PreEditEnded?.Invoke(this, EventArgs.Empty); + _currentContext?.OnPreEditEnded(); + } + + public void Shutdown() + { + Dispose(); + } + + public void Dispose() + { + if (_disposed) return; + _disposed = true; + + if (_xic != IntPtr.Zero) + { + XDestroyIC(_xic); + _xic = IntPtr.Zero; + } + + if (_xim != IntPtr.Zero) + { + XCloseIM(_xim); + _xim = IntPtr.Zero; + } + + // Note: Don't close display here if shared with window + } + + #region X11 Interop + + private const int KeyPress = 2; + private const int KeyRelease = 3; + + private const uint ShiftMask = 1 << 0; + private const uint LockMask = 1 << 1; + private const uint ControlMask = 1 << 2; + private const uint Mod1Mask = 1 << 3; // Alt + private const uint Mod2Mask = 1 << 4; // NumLock + private const uint Mod4Mask = 1 << 6; // Super + + private const long XIMPreeditNothing = 0x0008L; + private const long XIMPreeditCallbacks = 0x0002L; + private const long XIMStatusNothing = 0x0400L; + + private static readonly nint XNClientWindow = Marshal.StringToHGlobalAnsi("clientWindow"); + private static readonly nint XNFocusWindow = Marshal.StringToHGlobalAnsi("focusWindow"); + private static readonly nint XNInputStyle = Marshal.StringToHGlobalAnsi("inputStyle"); + private static readonly nint XNPreeditAttributes = Marshal.StringToHGlobalAnsi("preeditAttributes"); + private static readonly nint XNSpotLocation = Marshal.StringToHGlobalAnsi("spotLocation"); + + private delegate int XIMProc(nint xic, nint clientData, nint callData); + + [StructLayout(LayoutKind.Sequential)] + private struct XPoint + { + public short x; + public short y; + } + + [StructLayout(LayoutKind.Sequential)] + private struct XKeyEvent + { + public int type; + public ulong serial; + public bool send_event; + public nint display; + public nint window; + public nint root; + public nint subwindow; + public ulong time; + public int x, y; + public int x_root, y_root; + public uint state; + public uint keycode; + public bool same_screen; + } + + [DllImport("libX11.so.6")] + private static extern nint XOpenDisplay(nint display); + + [DllImport("libX11.so.6")] + private static extern nint XSetLocaleModifiers(string modifiers); + + [DllImport("libX11.so.6")] + private static extern nint XOpenIM(nint display, nint db, nint res_name, nint res_class); + + [DllImport("libX11.so.6")] + private static extern void XCloseIM(nint xim); + + [DllImport("libX11.so.6", EntryPoint = "XCreateIC")] + private static extern nint XCreateIC(nint xim, nint name1, nint value1, nint name2, nint value2, + nint name3, long value3, nint name4, nint value4, nint terminator); + + [DllImport("libX11.so.6", EntryPoint = "XCreateIC")] + private static extern nint XCreateICSimple(nint xim, nint name1, nint value1, nint name2, nint value2, + nint name3, long value3, nint terminator); + + [DllImport("libX11.so.6")] + private static extern void XDestroyIC(nint xic); + + [DllImport("libX11.so.6")] + private static extern void XSetICFocus(nint xic); + + [DllImport("libX11.so.6")] + private static extern void XUnsetICFocus(nint xic); + + [DllImport("libX11.so.6")] + private static extern nint XSetICValues(nint xic, nint name, nint value, nint terminator); + + [DllImport("libX11.so.6")] + private static extern nint XVaCreateNestedList(int unused, nint name, ref XPoint value, nint terminator); + + [DllImport("libX11.so.6")] + private static extern bool XFilterEvent(ref XKeyEvent xevent, nint window); + + [DllImport("libX11.so.6")] + private static extern int Xutf8LookupString(nint xic, ref XKeyEvent xevent, + byte[] buffer, int bytes, ref nint keySym, ref nint status); + + [DllImport("libX11.so.6")] + private static extern nint XmbResetIC(nint xic); + + [DllImport("libX11.so.6")] + private static extern void XFree(nint ptr); - private struct XPoint - { - public short x; - - public short y; - } - - private struct XKeyEvent - { - public int type; - - public ulong serial; - - public bool send_event; - - public IntPtr display; - - public IntPtr window; - - public IntPtr root; - - public IntPtr subwindow; - - public ulong time; - - public int x; - - public int y; - - public int x_root; - - public int y_root; - - public uint state; - - public uint keycode; - - public bool same_screen; - } - - private IntPtr _display; - - private IntPtr _window; - - private IntPtr _xim; - - private IntPtr _xic; - - private IInputContext? _currentContext; - - private string _preEditText = string.Empty; - - private int _preEditCursorPosition; - - private bool _isActive; - - private bool _disposed; - - private XIMProc? _preeditStartCallback; - - private XIMProc? _preeditDoneCallback; - - private XIMProc? _preeditDrawCallback; - - private XIMProc? _preeditCaretCallback; - - private XIMProc? _commitCallback; - - private const int KeyPress = 2; - - private const int KeyRelease = 3; - - private const uint ShiftMask = 1u; - - private const uint LockMask = 2u; - - private const uint ControlMask = 4u; - - private const uint Mod1Mask = 8u; - - private const uint Mod2Mask = 16u; - - private const uint Mod4Mask = 64u; - - private const long XIMPreeditNothing = 8L; - - private const long XIMPreeditCallbacks = 2L; - - private const long XIMStatusNothing = 1024L; - - private static readonly IntPtr XNClientWindow = Marshal.StringToHGlobalAnsi("clientWindow"); - - private static readonly IntPtr XNFocusWindow = Marshal.StringToHGlobalAnsi("focusWindow"); - - private static readonly IntPtr XNInputStyle = Marshal.StringToHGlobalAnsi("inputStyle"); - - private static readonly IntPtr XNPreeditAttributes = Marshal.StringToHGlobalAnsi("preeditAttributes"); - - private static readonly IntPtr XNSpotLocation = Marshal.StringToHGlobalAnsi("spotLocation"); - - public bool IsActive => _isActive; - - public string PreEditText => _preEditText; - - public int PreEditCursorPosition => _preEditCursorPosition; - - public event EventHandler? TextCommitted; - - public event EventHandler? PreEditChanged; - - public event EventHandler? PreEditEnded; - - public void Initialize(IntPtr windowHandle) - { - _window = windowHandle; - _display = XOpenDisplay(IntPtr.Zero); - if (_display == IntPtr.Zero) - { - Console.WriteLine("X11InputMethodService: Failed to open display"); - return; - } - if (XSetLocaleModifiers("") == IntPtr.Zero) - { - XSetLocaleModifiers("@im=none"); - } - _xim = XOpenIM(_display, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero); - if (_xim == IntPtr.Zero) - { - Console.WriteLine("X11InputMethodService: No input method available, trying IBus..."); - TryIBusFallback(); - } - else - { - CreateInputContext(); - } - } - - private void CreateInputContext() - { - if (_xim != IntPtr.Zero && _window != IntPtr.Zero) - { - IntPtr intPtr = CreatePreeditAttributes(); - _xic = XCreateIC(_xim, XNClientWindow, _window, XNFocusWindow, _window, XNInputStyle, 1026L, XNPreeditAttributes, intPtr, IntPtr.Zero); - if (intPtr != IntPtr.Zero) - { - XFree(intPtr); - } - if (_xic == IntPtr.Zero) - { - _xic = XCreateICSimple(_xim, XNClientWindow, _window, XNFocusWindow, _window, XNInputStyle, 1032L, IntPtr.Zero); - } - if (_xic != IntPtr.Zero) - { - Console.WriteLine("X11InputMethodService: Input context created successfully"); - } - } - } - - private IntPtr CreatePreeditAttributes() - { - _preeditStartCallback = PreeditStartCallback; - _preeditDoneCallback = PreeditDoneCallback; - _preeditDrawCallback = PreeditDrawCallback; - _preeditCaretCallback = PreeditCaretCallback; - return IntPtr.Zero; - } - - private int PreeditStartCallback(IntPtr xic, IntPtr clientData, IntPtr callData) - { - _isActive = true; - _preEditText = string.Empty; - _preEditCursorPosition = 0; - return -1; - } - - private int PreeditDoneCallback(IntPtr xic, IntPtr clientData, IntPtr callData) - { - _isActive = false; - _preEditText = string.Empty; - _preEditCursorPosition = 0; - this.PreEditEnded?.Invoke(this, EventArgs.Empty); - _currentContext?.OnPreEditEnded(); - return 0; - } - - private int PreeditDrawCallback(IntPtr xic, IntPtr clientData, IntPtr callData) - { - this.PreEditChanged?.Invoke(this, new PreEditChangedEventArgs(_preEditText, _preEditCursorPosition)); - _currentContext?.OnPreEditChanged(_preEditText, _preEditCursorPosition); - return 0; - } - - private int PreeditCaretCallback(IntPtr xic, IntPtr clientData, IntPtr callData) - { - return 0; - } - - private void TryIBusFallback() - { - Console.WriteLine("X11InputMethodService: IBus fallback not yet implemented"); - } - - public void SetFocus(IInputContext? context) - { - _currentContext = context; - if (_xic != IntPtr.Zero) - { - if (context != null) - { - XSetICFocus(_xic); - } - else - { - XUnsetICFocus(_xic); - } - } - } - - public void SetCursorLocation(int x, int y, int width, int height) - { - if (_xic != IntPtr.Zero) - { - XPoint value = new XPoint - { - x = (short)x, - y = (short)y - }; - IntPtr intPtr = XVaCreateNestedList(0, XNSpotLocation, ref value, IntPtr.Zero); - if (intPtr != IntPtr.Zero) - { - XSetICValues(_xic, XNPreeditAttributes, intPtr, IntPtr.Zero); - XFree(intPtr); - } - } - } - - public bool ProcessKeyEvent(uint keyCode, KeyModifiers modifiers, bool isKeyDown) - { - if (_xic == IntPtr.Zero) - { - return false; - } - XKeyEvent xevent = new XKeyEvent - { - type = (isKeyDown ? 2 : 3), - display = _display, - window = _window, - state = ConvertModifiers(modifiers), - keycode = keyCode - }; - if (XFilterEvent(ref xevent, _window)) - { - return true; - } - if (isKeyDown) - { - byte[] array = new byte[64]; - IntPtr keySym = IntPtr.Zero; - IntPtr status = IntPtr.Zero; - int num = Xutf8LookupString(_xic, ref xevent, array, array.Length, ref keySym, ref status); - if (num > 0) - { - string text = Encoding.UTF8.GetString(array, 0, num); - OnTextCommit(text); - return true; - } - } - return false; - } - - private void OnTextCommit(string text) - { - _preEditText = string.Empty; - _preEditCursorPosition = 0; - this.TextCommitted?.Invoke(this, new TextCommittedEventArgs(text)); - _currentContext?.OnTextCommitted(text); - } - - private uint ConvertModifiers(KeyModifiers modifiers) - { - uint num = 0u; - if (modifiers.HasFlag(KeyModifiers.Shift)) - { - num |= 1; - } - if (modifiers.HasFlag(KeyModifiers.Control)) - { - num |= 4; - } - if (modifiers.HasFlag(KeyModifiers.Alt)) - { - num |= 8; - } - if (modifiers.HasFlag(KeyModifiers.Super)) - { - num |= 0x40; - } - if (modifiers.HasFlag(KeyModifiers.CapsLock)) - { - num |= 2; - } - if (modifiers.HasFlag(KeyModifiers.NumLock)) - { - num |= 0x10; - } - return num; - } - - public void Reset() - { - if (_xic != IntPtr.Zero) - { - XmbResetIC(_xic); - } - _preEditText = string.Empty; - _preEditCursorPosition = 0; - _isActive = false; - this.PreEditEnded?.Invoke(this, EventArgs.Empty); - _currentContext?.OnPreEditEnded(); - } - - public void Shutdown() - { - Dispose(); - } - - public void Dispose() - { - if (!_disposed) - { - _disposed = true; - if (_xic != IntPtr.Zero) - { - XDestroyIC(_xic); - _xic = IntPtr.Zero; - } - if (_xim != IntPtr.Zero) - { - XCloseIM(_xim); - _xim = IntPtr.Zero; - } - } - } - - [DllImport("libX11.so.6")] - private static extern IntPtr XOpenDisplay(IntPtr display); - - [DllImport("libX11.so.6")] - private static extern IntPtr XSetLocaleModifiers(string modifiers); - - [DllImport("libX11.so.6")] - private static extern IntPtr XOpenIM(IntPtr display, IntPtr db, IntPtr res_name, IntPtr res_class); - - [DllImport("libX11.so.6")] - private static extern void XCloseIM(IntPtr xim); - - [DllImport("libX11.so.6")] - private static extern IntPtr XCreateIC(IntPtr xim, IntPtr name1, IntPtr value1, IntPtr name2, IntPtr value2, IntPtr name3, long value3, IntPtr name4, IntPtr value4, IntPtr terminator); - - [DllImport("libX11.so.6", EntryPoint = "XCreateIC")] - private static extern IntPtr XCreateICSimple(IntPtr xim, IntPtr name1, IntPtr value1, IntPtr name2, IntPtr value2, IntPtr name3, long value3, IntPtr terminator); - - [DllImport("libX11.so.6")] - private static extern void XDestroyIC(IntPtr xic); - - [DllImport("libX11.so.6")] - private static extern void XSetICFocus(IntPtr xic); - - [DllImport("libX11.so.6")] - private static extern void XUnsetICFocus(IntPtr xic); - - [DllImport("libX11.so.6")] - private static extern IntPtr XSetICValues(IntPtr xic, IntPtr name, IntPtr value, IntPtr terminator); - - [DllImport("libX11.so.6")] - private static extern IntPtr XVaCreateNestedList(int unused, IntPtr name, ref XPoint value, IntPtr terminator); - - [DllImport("libX11.so.6")] - private static extern bool XFilterEvent(ref XKeyEvent xevent, IntPtr window); - - [DllImport("libX11.so.6")] - private static extern int Xutf8LookupString(IntPtr xic, ref XKeyEvent xevent, byte[] buffer, int bytes, ref IntPtr keySym, ref IntPtr status); - - [DllImport("libX11.so.6")] - private static extern IntPtr XmbResetIC(IntPtr xic); - - [DllImport("libX11.so.6")] - private static extern void XFree(IntPtr ptr); + #endregion } diff --git a/Views/LinuxWebView.cs b/Views/LinuxWebView.cs deleted file mode 100644 index 6e546d0..0000000 --- a/Views/LinuxWebView.cs +++ /dev/null @@ -1,490 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.Maui.Platform.Linux.Interop; -using SkiaSharp; - -namespace Microsoft.Maui.Platform; - -/// -/// Linux platform WebView using WebKitGTK. -/// This is a native widget overlay that renders on top of the Skia surface. -/// -public class LinuxWebView : SkiaView -{ - private IntPtr _webView; - private IntPtr _gtkWindow; - private bool _initialized; - private bool _isVisible = true; - private string? _currentUrl; - private string? _userAgent; - - // Signal handler IDs for cleanup - private ulong _loadChangedHandlerId; - private ulong _decidePolicyHandlerId; - private ulong _titleChangedHandlerId; - - // Keep delegates alive to prevent GC - private WebKitGtk.LoadChangedCallback? _loadChangedCallback; - private WebKitGtk.DecidePolicyCallback? _decidePolicyCallback; - private WebKitGtk.NotifyCallback? _titleChangedCallback; - - /// - /// Event raised when navigation starts. - /// - public event EventHandler? Navigating; - - /// - /// Event raised when navigation completes. - /// - public event EventHandler? Navigated; - - /// - /// Event raised when the page title changes. - /// - public event EventHandler? TitleChanged; - - /// - /// Gets whether the WebView can navigate back. - /// - public bool CanGoBack => _webView != IntPtr.Zero && WebKitGtk.webkit_web_view_can_go_back(_webView); - - /// - /// Gets whether the WebView can navigate forward. - /// - public bool CanGoForward => _webView != IntPtr.Zero && WebKitGtk.webkit_web_view_can_go_forward(_webView); - - /// - /// Gets the current URL. - /// - public string? CurrentUrl - { - get - { - if (_webView == IntPtr.Zero) - return _currentUrl; - - var uriPtr = WebKitGtk.webkit_web_view_get_uri(_webView); - return WebKitGtk.PtrToStringUtf8(uriPtr) ?? _currentUrl; - } - } - - /// - /// Gets or sets the user agent string. - /// - public string? UserAgent - { - get => _userAgent; - set - { - _userAgent = value; - if (_webView != IntPtr.Zero && value != null) - { - var settings = WebKitGtk.webkit_web_view_get_settings(_webView); - WebKitGtk.webkit_settings_set_user_agent(settings, value); - } - } - } - - public LinuxWebView() - { - // WebView will be initialized when first shown or when source is set - } - - /// - /// Initializes the WebKitGTK WebView. - /// - private void EnsureInitialized() - { - if (_initialized) - return; - - try - { - // Initialize GTK if not already done - int argc = 0; - IntPtr argv = IntPtr.Zero; - WebKitGtk.gtk_init_check(ref argc, ref argv); - - // Create a top-level window to host the WebView - // GTK_WINDOW_TOPLEVEL = 0 - _gtkWindow = WebKitGtk.gtk_window_new(0); - if (_gtkWindow == IntPtr.Zero) - { - Console.WriteLine("[LinuxWebView] Failed to create GTK window"); - return; - } - - // Configure the window - WebKitGtk.gtk_window_set_decorated(_gtkWindow, false); - WebKitGtk.gtk_widget_set_can_focus(_gtkWindow, true); - - // Create the WebKit WebView - _webView = WebKitGtk.webkit_web_view_new(); - if (_webView == IntPtr.Zero) - { - Console.WriteLine("[LinuxWebView] Failed to create WebKit WebView"); - WebKitGtk.gtk_widget_destroy(_gtkWindow); - _gtkWindow = IntPtr.Zero; - return; - } - - // Configure settings - var settings = WebKitGtk.webkit_web_view_get_settings(_webView); - WebKitGtk.webkit_settings_set_enable_javascript(settings, true); - WebKitGtk.webkit_settings_set_enable_webgl(settings, true); - WebKitGtk.webkit_settings_set_enable_developer_extras(settings, true); - WebKitGtk.webkit_settings_set_javascript_can_access_clipboard(settings, true); - - if (_userAgent != null) - { - WebKitGtk.webkit_settings_set_user_agent(settings, _userAgent); - } - - // Connect signals - ConnectSignals(); - - // Add WebView to window - WebKitGtk.gtk_container_add(_gtkWindow, _webView); - - _initialized = true; - Console.WriteLine("[LinuxWebView] WebKitGTK WebView initialized successfully"); - } - catch (Exception ex) - { - Console.WriteLine($"[LinuxWebView] Initialization failed: {ex.Message}"); - Console.WriteLine($"[LinuxWebView] Make sure WebKitGTK is installed: sudo apt install libwebkit2gtk-4.1-0"); - } - } - - private void ConnectSignals() - { - // Keep callbacks alive - _loadChangedCallback = OnLoadChanged; - _decidePolicyCallback = OnDecidePolicy; - _titleChangedCallback = OnTitleChanged; - - // Connect load-changed signal - _loadChangedHandlerId = WebKitGtk.g_signal_connect_data( - _webView, "load-changed", _loadChangedCallback, IntPtr.Zero, IntPtr.Zero, 0); - - // Connect decide-policy signal for navigation control - _decidePolicyHandlerId = WebKitGtk.g_signal_connect_data( - _webView, "decide-policy", _decidePolicyCallback, IntPtr.Zero, IntPtr.Zero, 0); - - // Connect notify::title for title changes - _titleChangedHandlerId = WebKitGtk.g_signal_connect_data( - _webView, "notify::title", _titleChangedCallback, IntPtr.Zero, IntPtr.Zero, 0); - } - - private void OnLoadChanged(IntPtr webView, int loadEvent, IntPtr userData) - { - var url = CurrentUrl ?? ""; - - switch (loadEvent) - { - case WebKitGtk.WEBKIT_LOAD_STARTED: - case WebKitGtk.WEBKIT_LOAD_REDIRECTED: - Navigating?.Invoke(this, new WebViewNavigatingEventArgs(url)); - break; - - case WebKitGtk.WEBKIT_LOAD_FINISHED: - Navigated?.Invoke(this, new WebViewNavigatedEventArgs(url, true)); - break; - - case WebKitGtk.WEBKIT_LOAD_COMMITTED: - // Page content has started loading - break; - } - } - - private bool OnDecidePolicy(IntPtr webView, IntPtr decision, int decisionType, IntPtr userData) - { - if (decisionType == WebKitGtk.WEBKIT_POLICY_DECISION_TYPE_NAVIGATION_ACTION) - { - var action = WebKitGtk.webkit_navigation_action_get_request(decision); - var uriPtr = WebKitGtk.webkit_uri_request_get_uri(action); - var url = WebKitGtk.PtrToStringUtf8(uriPtr) ?? ""; - - var args = new WebViewNavigatingEventArgs(url); - Navigating?.Invoke(this, args); - - if (args.Cancel) - { - WebKitGtk.webkit_policy_decision_ignore(decision); - return true; - } - } - - WebKitGtk.webkit_policy_decision_use(decision); - return true; - } - - private void OnTitleChanged(IntPtr webView, IntPtr paramSpec, IntPtr userData) - { - var titlePtr = WebKitGtk.webkit_web_view_get_title(_webView); - var title = WebKitGtk.PtrToStringUtf8(titlePtr); - TitleChanged?.Invoke(this, title); - } - - /// - /// Navigates to the specified URL. - /// - public void LoadUrl(string url) - { - EnsureInitialized(); - if (_webView == IntPtr.Zero) - return; - - _currentUrl = url; - WebKitGtk.webkit_web_view_load_uri(_webView, url); - UpdateWindowPosition(); - ShowWebView(); - } - - /// - /// Loads HTML content. - /// - public void LoadHtml(string html, string? baseUrl = null) - { - EnsureInitialized(); - if (_webView == IntPtr.Zero) - return; - - WebKitGtk.webkit_web_view_load_html(_webView, html, baseUrl); - UpdateWindowPosition(); - ShowWebView(); - } - - /// - /// Navigates back in history. - /// - public void GoBack() - { - if (_webView != IntPtr.Zero && CanGoBack) - { - WebKitGtk.webkit_web_view_go_back(_webView); - } - } - - /// - /// Navigates forward in history. - /// - public void GoForward() - { - if (_webView != IntPtr.Zero && CanGoForward) - { - WebKitGtk.webkit_web_view_go_forward(_webView); - } - } - - /// - /// Reloads the current page. - /// - public void Reload() - { - if (_webView != IntPtr.Zero) - { - WebKitGtk.webkit_web_view_reload(_webView); - } - } - - /// - /// Stops loading the current page. - /// - public void Stop() - { - if (_webView != IntPtr.Zero) - { - WebKitGtk.webkit_web_view_stop_loading(_webView); - } - } - - /// - /// Evaluates JavaScript and returns the result. - /// - public Task EvaluateJavaScriptAsync(string script) - { - var tcs = new TaskCompletionSource(); - - if (_webView == IntPtr.Zero) - { - tcs.SetResult(null); - return tcs.Task; - } - - // For now, use fire-and-forget JavaScript execution - // Full async result handling requires GAsyncReadyCallback marshaling - WebKitGtk.webkit_web_view_run_javascript(_webView, script, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero); - tcs.SetResult(null); // Return null for now, full implementation needs async callback - - return tcs.Task; - } - - /// - /// Evaluates JavaScript without waiting for result. - /// - public void Eval(string script) - { - if (_webView != IntPtr.Zero) - { - WebKitGtk.webkit_web_view_run_javascript(_webView, script, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero); - } - } - - private void ShowWebView() - { - if (_gtkWindow != IntPtr.Zero && _isVisible) - { - WebKitGtk.gtk_widget_show_all(_gtkWindow); - } - } - - private void HideWebView() - { - if (_gtkWindow != IntPtr.Zero) - { - WebKitGtk.gtk_widget_hide(_gtkWindow); - } - } - - private void UpdateWindowPosition() - { - if (_gtkWindow == IntPtr.Zero) - return; - - // Get the screen position of this view's bounds - var bounds = Bounds; - var screenX = (int)bounds.Left; - var screenY = (int)bounds.Top; - var width = (int)bounds.Width; - var height = (int)bounds.Height; - - if (width > 0 && height > 0) - { - WebKitGtk.gtk_window_move(_gtkWindow, screenX, screenY); - WebKitGtk.gtk_window_resize(_gtkWindow, width, height); - } - } - - protected override void OnBoundsChanged() - { - base.OnBoundsChanged(); - UpdateWindowPosition(); - } - - protected override void OnVisibilityChanged() - { - base.OnVisibilityChanged(); - _isVisible = IsVisible; - - if (_isVisible) - { - ShowWebView(); - } - else - { - HideWebView(); - } - } - - protected override void OnDraw(SKCanvas canvas, SKRect bounds) - { - // Draw a placeholder rectangle where the WebView will be overlaid - using var paint = new SKPaint - { - Color = new SKColor(240, 240, 240), - Style = SKPaintStyle.Fill - }; - canvas.DrawRect(bounds, paint); - - // Draw border - using var borderPaint = new SKPaint - { - Color = new SKColor(200, 200, 200), - Style = SKPaintStyle.Stroke, - StrokeWidth = 1 - }; - canvas.DrawRect(bounds, borderPaint); - - // Draw "WebView" label if not yet initialized - if (!_initialized) - { - using var textPaint = new SKPaint - { - Color = SKColors.Gray, - TextSize = 14, - IsAntialias = true - }; - var text = "WebView (WebKitGTK)"; - var textBounds = new SKRect(); - textPaint.MeasureText(text, ref textBounds); - var x = bounds.MidX - textBounds.MidX; - var y = bounds.MidY - textBounds.MidY; - canvas.DrawText(text, x, y, textPaint); - } - - // Process GTK events to keep WebView responsive - WebKitGtk.ProcessGtkEvents(); - } - - protected override void Dispose(bool disposing) - { - if (disposing) - { - // Disconnect signals - if (_webView != IntPtr.Zero) - { - if (_loadChangedHandlerId != 0) - WebKitGtk.g_signal_handler_disconnect(_webView, _loadChangedHandlerId); - if (_decidePolicyHandlerId != 0) - WebKitGtk.g_signal_handler_disconnect(_webView, _decidePolicyHandlerId); - if (_titleChangedHandlerId != 0) - WebKitGtk.g_signal_handler_disconnect(_webView, _titleChangedHandlerId); - } - - // Destroy widgets - if (_gtkWindow != IntPtr.Zero) - { - WebKitGtk.gtk_widget_destroy(_gtkWindow); - _gtkWindow = IntPtr.Zero; - _webView = IntPtr.Zero; // WebView is destroyed with window - } - - _loadChangedCallback = null; - _decidePolicyCallback = null; - _titleChangedCallback = null; - } - - base.Dispose(disposing); - } -} - -/// -/// Event args for WebView navigation starting. -/// -public class WebViewNavigatingEventArgs : EventArgs -{ - public string Url { get; } - public bool Cancel { get; set; } - - public WebViewNavigatingEventArgs(string url) - { - Url = url; - } -} - -/// -/// Event args for WebView navigation completed. -/// -public class WebViewNavigatedEventArgs : EventArgs -{ - public string Url { get; } - public bool Success { get; } - - public WebViewNavigatedEventArgs(string url, bool success) - { - Url = url; - Success = success; - } -} diff --git a/Views/SkiaAbsoluteLayout.cs b/Views/SkiaAbsoluteLayout.cs deleted file mode 100644 index f9f4792..0000000 --- a/Views/SkiaAbsoluteLayout.cs +++ /dev/null @@ -1,143 +0,0 @@ -using System; -using System.Collections.Generic; -using SkiaSharp; - -namespace Microsoft.Maui.Platform; - -public class SkiaAbsoluteLayout : SkiaLayoutView -{ - private readonly Dictionary _childBounds = new Dictionary(); - - public void AddChild(SkiaView child, SKRect bounds, AbsoluteLayoutFlags flags = AbsoluteLayoutFlags.None) - { - //IL_000e: Unknown result type (might be due to invalid IL or missing references) - base.AddChild(child); - _childBounds[child] = new AbsoluteLayoutBounds(bounds, flags); - } - - public override void RemoveChild(SkiaView child) - { - base.RemoveChild(child); - _childBounds.Remove(child); - } - - public AbsoluteLayoutBounds GetLayoutBounds(SkiaView child) - { - //IL_0010: Unknown result type (might be due to invalid IL or missing references) - if (!_childBounds.TryGetValue(child, out var value)) - { - return new AbsoluteLayoutBounds(SKRect.Empty, AbsoluteLayoutFlags.None); - } - return value; - } - - public void SetLayoutBounds(SkiaView child, SKRect bounds, AbsoluteLayoutFlags flags = AbsoluteLayoutFlags.None) - { - //IL_0007: Unknown result type (might be due to invalid IL or missing references) - _childBounds[child] = new AbsoluteLayoutBounds(bounds, flags); - InvalidateMeasure(); - Invalidate(); - } - - protected override SKSize MeasureOverride(SKSize availableSize) - { - //IL_0034: Unknown result type (might be due to invalid IL or missing references) - //IL_0039: Unknown result type (might be due to invalid IL or missing references) - //IL_004a: Unknown result type (might be due to invalid IL or missing references) - //IL_004f: Unknown result type (might be due to invalid IL or missing references) - //IL_0087: Unknown result type (might be due to invalid IL or missing references) - //IL_008c: Unknown result type (might be due to invalid IL or missing references) - //IL_0097: Unknown result type (might be due to invalid IL or missing references) - //IL_009c: Unknown result type (might be due to invalid IL or missing references) - //IL_00a8: Unknown result type (might be due to invalid IL or missing references) - //IL_00ad: Unknown result type (might be due to invalid IL or missing references) - //IL_00b8: Unknown result type (might be due to invalid IL or missing references) - //IL_00bd: Unknown result type (might be due to invalid IL or missing references) - //IL_00c7: Unknown result type (might be due to invalid IL or missing references) - float num = 0f; - float num2 = 0f; - foreach (SkiaView child in base.Children) - { - if (child.IsVisible) - { - SKRect bounds = GetLayoutBounds(child).Bounds; - child.Measure(new SKSize(((SKRect)(ref bounds)).Width, ((SKRect)(ref bounds)).Height)); - num = Math.Max(num, ((SKRect)(ref bounds)).Right); - num2 = Math.Max(num2, ((SKRect)(ref bounds)).Bottom); - } - } - float num3 = num; - SKRect padding = base.Padding; - float num4 = num3 + ((SKRect)(ref padding)).Left; - padding = base.Padding; - float num5 = num4 + ((SKRect)(ref padding)).Right; - float num6 = num2; - padding = base.Padding; - float num7 = num6 + ((SKRect)(ref padding)).Top; - padding = base.Padding; - return new SKSize(num5, num7 + ((SKRect)(ref padding)).Bottom); - } - - protected override SKRect ArrangeOverride(SKRect bounds) - { - //IL_0001: Unknown result type (might be due to invalid IL or missing references) - //IL_0002: Unknown result type (might be due to invalid IL or missing references) - //IL_0007: Unknown result type (might be due to invalid IL or missing references) - //IL_0035: Unknown result type (might be due to invalid IL or missing references) - //IL_003a: Unknown result type (might be due to invalid IL or missing references) - //IL_01c7: Unknown result type (might be due to invalid IL or missing references) - //IL_00fb: Unknown result type (might be due to invalid IL or missing references) - //IL_0100: Unknown result type (might be due to invalid IL or missing references) - //IL_014c: Unknown result type (might be due to invalid IL or missing references) - //IL_0151: Unknown result type (might be due to invalid IL or missing references) - //IL_0168: Unknown result type (might be due to invalid IL or missing references) - //IL_016d: Unknown result type (might be due to invalid IL or missing references) - //IL_01a9: Unknown result type (might be due to invalid IL or missing references) - SKRect contentBounds = GetContentBounds(bounds); - SKRect bounds3 = default(SKRect); - foreach (SkiaView child in base.Children) - { - if (child.IsVisible) - { - AbsoluteLayoutBounds layoutBounds = GetLayoutBounds(child); - SKRect bounds2 = layoutBounds.Bounds; - AbsoluteLayoutFlags flags = layoutBounds.Flags; - float num = ((!flags.HasFlag(AbsoluteLayoutFlags.XProportional)) ? (((SKRect)(ref contentBounds)).Left + ((SKRect)(ref bounds2)).Left) : (((SKRect)(ref contentBounds)).Left + ((SKRect)(ref bounds2)).Left * ((SKRect)(ref contentBounds)).Width)); - float num2 = ((!flags.HasFlag(AbsoluteLayoutFlags.YProportional)) ? (((SKRect)(ref contentBounds)).Top + ((SKRect)(ref bounds2)).Top) : (((SKRect)(ref contentBounds)).Top + ((SKRect)(ref bounds2)).Top * ((SKRect)(ref contentBounds)).Height)); - float num3; - SKSize desiredSize; - if (flags.HasFlag(AbsoluteLayoutFlags.WidthProportional)) - { - num3 = ((SKRect)(ref bounds2)).Width * ((SKRect)(ref contentBounds)).Width; - } - else if (((SKRect)(ref bounds2)).Width < 0f) - { - desiredSize = child.DesiredSize; - num3 = ((SKSize)(ref desiredSize)).Width; - } - else - { - num3 = ((SKRect)(ref bounds2)).Width; - } - float num4; - if (flags.HasFlag(AbsoluteLayoutFlags.HeightProportional)) - { - num4 = ((SKRect)(ref bounds2)).Height * ((SKRect)(ref contentBounds)).Height; - } - else if (((SKRect)(ref bounds2)).Height < 0f) - { - desiredSize = child.DesiredSize; - num4 = ((SKSize)(ref desiredSize)).Height; - } - else - { - num4 = ((SKRect)(ref bounds2)).Height; - } - Thickness margin = child.Margin; - ((SKRect)(ref bounds3))._002Ector(num + (float)((Thickness)(ref margin)).Left, num2 + (float)((Thickness)(ref margin)).Top, num + num3 - (float)((Thickness)(ref margin)).Right, num2 + num4 - (float)((Thickness)(ref margin)).Bottom); - child.Arrange(bounds3); - } - } - return bounds; - } -} diff --git a/Views/SkiaActivityIndicator.cs b/Views/SkiaActivityIndicator.cs index 821cb10..bdf374c 100644 --- a/Views/SkiaActivityIndicator.cs +++ b/Views/SkiaActivityIndicator.cs @@ -1,222 +1,238 @@ -using System; -using Microsoft.Maui.Controls; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using SkiaSharp; namespace Microsoft.Maui.Platform; +/// +/// Skia-rendered activity indicator (spinner) control with full XAML styling support. +/// public class SkiaActivityIndicator : SkiaView { - public static readonly BindableProperty IsRunningProperty = BindableProperty.Create("IsRunning", typeof(bool), typeof(SkiaActivityIndicator), (object)false, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaActivityIndicator)(object)b).OnIsRunningChanged(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + #region BindableProperties - public static readonly BindableProperty ColorProperty = BindableProperty.Create("Color", typeof(SKColor), typeof(SkiaActivityIndicator), (object)new SKColor((byte)33, (byte)150, (byte)243), (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaActivityIndicator)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for IsRunning. + /// + public static readonly BindableProperty IsRunningProperty = + BindableProperty.Create( + nameof(IsRunning), + typeof(bool), + typeof(SkiaActivityIndicator), + false, + propertyChanged: (b, o, n) => ((SkiaActivityIndicator)b).OnIsRunningChanged()); - public static readonly BindableProperty DisabledColorProperty = BindableProperty.Create("DisabledColor", typeof(SKColor), typeof(SkiaActivityIndicator), (object)new SKColor((byte)189, (byte)189, (byte)189), (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaActivityIndicator)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for Color. + /// + public static readonly BindableProperty ColorProperty = + BindableProperty.Create( + nameof(Color), + typeof(SKColor), + typeof(SkiaActivityIndicator), + new SKColor(0x21, 0x96, 0xF3), + propertyChanged: (b, o, n) => ((SkiaActivityIndicator)b).Invalidate()); - public static readonly BindableProperty SizeProperty = BindableProperty.Create("Size", typeof(float), typeof(SkiaActivityIndicator), (object)32f, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaActivityIndicator)(object)b).InvalidateMeasure(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for DisabledColor. + /// + public static readonly BindableProperty DisabledColorProperty = + BindableProperty.Create( + nameof(DisabledColor), + typeof(SKColor), + typeof(SkiaActivityIndicator), + new SKColor(0xBD, 0xBD, 0xBD), + propertyChanged: (b, o, n) => ((SkiaActivityIndicator)b).Invalidate()); - public static readonly BindableProperty StrokeWidthProperty = BindableProperty.Create("StrokeWidth", typeof(float), typeof(SkiaActivityIndicator), (object)3f, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaActivityIndicator)(object)b).InvalidateMeasure(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for Size. + /// + public static readonly BindableProperty SizeProperty = + BindableProperty.Create( + nameof(Size), + typeof(float), + typeof(SkiaActivityIndicator), + 32f, + propertyChanged: (b, o, n) => ((SkiaActivityIndicator)b).InvalidateMeasure()); - public static readonly BindableProperty RotationSpeedProperty = BindableProperty.Create("RotationSpeed", typeof(float), typeof(SkiaActivityIndicator), (object)360f, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)null, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for StrokeWidth. + /// + public static readonly BindableProperty StrokeWidthProperty = + BindableProperty.Create( + nameof(StrokeWidth), + typeof(float), + typeof(SkiaActivityIndicator), + 3f, + propertyChanged: (b, o, n) => ((SkiaActivityIndicator)b).InvalidateMeasure()); - public static readonly BindableProperty ArcCountProperty = BindableProperty.Create("ArcCount", typeof(int), typeof(SkiaActivityIndicator), (object)12, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaActivityIndicator)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for RotationSpeed. + /// + public static readonly BindableProperty RotationSpeedProperty = + BindableProperty.Create( + nameof(RotationSpeed), + typeof(float), + typeof(SkiaActivityIndicator), + 360f); - private float _rotationAngle; + /// + /// Bindable property for ArcCount. + /// + public static readonly BindableProperty ArcCountProperty = + BindableProperty.Create( + nameof(ArcCount), + typeof(int), + typeof(SkiaActivityIndicator), + 12, + propertyChanged: (b, o, n) => ((SkiaActivityIndicator)b).Invalidate()); - private DateTime _lastUpdateTime = DateTime.UtcNow; + #endregion - public bool IsRunning - { - get - { - return (bool)((BindableObject)this).GetValue(IsRunningProperty); - } - set - { - ((BindableObject)this).SetValue(IsRunningProperty, (object)value); - } - } + #region Properties - public SKColor Color - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKColor)((BindableObject)this).GetValue(ColorProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(ColorProperty, (object)value); - } - } + /// + /// Gets or sets whether the indicator is running. + /// + public bool IsRunning + { + get => (bool)GetValue(IsRunningProperty); + set => SetValue(IsRunningProperty, value); + } - public SKColor DisabledColor - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKColor)((BindableObject)this).GetValue(DisabledColorProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(DisabledColorProperty, (object)value); - } - } + /// + /// Gets or sets the indicator color. + /// + public SKColor Color + { + get => (SKColor)GetValue(ColorProperty); + set => SetValue(ColorProperty, value); + } - public float Size - { - get - { - return (float)((BindableObject)this).GetValue(SizeProperty); - } - set - { - ((BindableObject)this).SetValue(SizeProperty, (object)value); - } - } + /// + /// Gets or sets the disabled color. + /// + public SKColor DisabledColor + { + get => (SKColor)GetValue(DisabledColorProperty); + set => SetValue(DisabledColorProperty, value); + } - public float StrokeWidth - { - get - { - return (float)((BindableObject)this).GetValue(StrokeWidthProperty); - } - set - { - ((BindableObject)this).SetValue(StrokeWidthProperty, (object)value); - } - } + /// + /// Gets or sets the indicator size. + /// + public float Size + { + get => (float)GetValue(SizeProperty); + set => SetValue(SizeProperty, value); + } - public float RotationSpeed - { - get - { - return (float)((BindableObject)this).GetValue(RotationSpeedProperty); - } - set - { - ((BindableObject)this).SetValue(RotationSpeedProperty, (object)value); - } - } + /// + /// Gets or sets the stroke width. + /// + public float StrokeWidth + { + get => (float)GetValue(StrokeWidthProperty); + set => SetValue(StrokeWidthProperty, value); + } - public int ArcCount - { - get - { - return (int)((BindableObject)this).GetValue(ArcCountProperty); - } - set - { - ((BindableObject)this).SetValue(ArcCountProperty, (object)value); - } - } + /// + /// Gets or sets the rotation speed in degrees per second. + /// + public float RotationSpeed + { + get => (float)GetValue(RotationSpeedProperty); + set => SetValue(RotationSpeedProperty, value); + } - private void OnIsRunningChanged() - { - if (IsRunning) - { - _lastUpdateTime = DateTime.UtcNow; - } - Invalidate(); - } + /// + /// Gets or sets the number of arcs. + /// + public int ArcCount + { + get => (int)GetValue(ArcCountProperty); + set => SetValue(ArcCountProperty, value); + } - protected override void OnDraw(SKCanvas canvas, SKRect bounds) - { - //IL_00cc: Unknown result type (might be due to invalid IL or missing references) - //IL_00c4: Unknown result type (might be due to invalid IL or missing references) - //IL_00d1: Unknown result type (might be due to invalid IL or missing references) - //IL_00f8: Unknown result type (might be due to invalid IL or missing references) - //IL_00fd: Unknown result type (might be due to invalid IL or missing references) - //IL_00ff: Unknown result type (might be due to invalid IL or missing references) - //IL_0104: Unknown result type (might be due to invalid IL or missing references) - //IL_0105: Unknown result type (might be due to invalid IL or missing references) - //IL_010c: Unknown result type (might be due to invalid IL or missing references) - //IL_0113: Unknown result type (might be due to invalid IL or missing references) - //IL_011a: Unknown result type (might be due to invalid IL or missing references) - //IL_0126: Unknown result type (might be due to invalid IL or missing references) - //IL_012f: Expected O, but got Unknown - //IL_0157: Unknown result type (might be due to invalid IL or missing references) - //IL_015e: Expected O, but got Unknown - //IL_0166: Unknown result type (might be due to invalid IL or missing references) - if (!IsRunning && !base.IsEnabled) - { - return; - } - float midX = ((SKRect)(ref bounds)).MidX; - float midY = ((SKRect)(ref bounds)).MidY; - float num = Math.Min(Size / 2f, Math.Min(((SKRect)(ref bounds)).Width, ((SKRect)(ref bounds)).Height) / 2f) - StrokeWidth; - if (IsRunning) - { - DateTime utcNow = DateTime.UtcNow; - double totalSeconds = (utcNow - _lastUpdateTime).TotalSeconds; - _lastUpdateTime = utcNow; - _rotationAngle = (_rotationAngle + (float)((double)RotationSpeed * totalSeconds)) % 360f; - } - canvas.Save(); - canvas.Translate(midX, midY); - canvas.RotateDegrees(_rotationAngle); - SKColor val = (base.IsEnabled ? Color : DisabledColor); - for (int i = 0; i < ArcCount; i++) - { - byte b = (byte)(255f * (1f - (float)i / (float)ArcCount)); - SKColor color = ((SKColor)(ref val)).WithAlpha(b); - SKPaint val2 = new SKPaint - { - Color = color, - IsAntialias = true, - Style = (SKPaintStyle)1, - StrokeWidth = StrokeWidth, - StrokeCap = (SKStrokeCap)1 - }; - try - { - float num2 = 360f / (float)ArcCount * (float)i; - float num3 = 360f / (float)ArcCount / 2f; - SKPath val3 = new SKPath(); - try - { - val3.AddArc(new SKRect(0f - num, 0f - num, num, num), num2, num3); - canvas.DrawPath(val3, val2); - } - finally - { - ((IDisposable)val3)?.Dispose(); - } - } - finally - { - ((IDisposable)val2)?.Dispose(); - } - } - canvas.Restore(); - if (IsRunning) - { - Invalidate(); - } - } + #endregion - protected override SKSize MeasureOverride(SKSize availableSize) - { - //IL_0026: Unknown result type (might be due to invalid IL or missing references) - return new SKSize(Size + StrokeWidth * 2f, Size + StrokeWidth * 2f); - } + private float _rotationAngle; + private DateTime _lastUpdateTime = DateTime.UtcNow; + + private void OnIsRunningChanged() + { + if (IsRunning) + { + _lastUpdateTime = DateTime.UtcNow; + } + Invalidate(); + } + + protected override void OnDraw(SKCanvas canvas, SKRect bounds) + { + if (!IsRunning && !IsEnabled) + { + return; + } + + var centerX = bounds.MidX; + var centerY = bounds.MidY; + var radius = Math.Min(Size / 2, Math.Min(bounds.Width, bounds.Height) / 2) - StrokeWidth; + + // Update rotation + if (IsRunning) + { + var now = DateTime.UtcNow; + var elapsed = (now - _lastUpdateTime).TotalSeconds; + _lastUpdateTime = now; + _rotationAngle = (_rotationAngle + (float)(RotationSpeed * elapsed)) % 360; + } + + canvas.Save(); + canvas.Translate(centerX, centerY); + canvas.RotateDegrees(_rotationAngle); + + var color = IsEnabled ? Color : DisabledColor; + + // Draw arcs with varying opacity + for (int i = 0; i < ArcCount; i++) + { + var alpha = (byte)(255 * (1 - (float)i / ArcCount)); + var arcColor = color.WithAlpha(alpha); + + using var paint = new SKPaint + { + Color = arcColor, + IsAntialias = true, + Style = SKPaintStyle.Stroke, + StrokeWidth = StrokeWidth, + StrokeCap = SKStrokeCap.Round + }; + + var startAngle = (360f / ArcCount) * i; + var sweepAngle = 360f / ArcCount / 2; + + using var path = new SKPath(); + path.AddArc( + new SKRect(-radius, -radius, radius, radius), + startAngle, + sweepAngle); + + canvas.DrawPath(path, paint); + } + + canvas.Restore(); + + // Request redraw for animation + if (IsRunning) + { + Invalidate(); + } + } + + protected override SKSize MeasureOverride(SKSize availableSize) + { + return new SKSize(Size + StrokeWidth * 2, Size + StrokeWidth * 2); + } } diff --git a/Views/SkiaAlertDialog.cs b/Views/SkiaAlertDialog.cs index b8e9246..92ba9d6 100644 --- a/Views/SkiaAlertDialog.cs +++ b/Views/SkiaAlertDialog.cs @@ -1,440 +1,385 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using SkiaSharp; namespace Microsoft.Maui.Platform; +/// +/// A modal alert dialog rendered with Skia. +/// Supports title, message, and up to two buttons (cancel/accept). +/// public class SkiaAlertDialog : SkiaView { - private readonly string _title; + private readonly string _title; + private readonly string _message; + private readonly string? _cancel; + private readonly string? _accept; + private readonly TaskCompletionSource _tcs; - private readonly string _message; + private SKRect _cancelButtonBounds; + private SKRect _acceptButtonBounds; + private bool _cancelHovered; + private bool _acceptHovered; - private readonly string? _cancel; + // Dialog styling + private static readonly SKColor OverlayColor = new SKColor(0, 0, 0, 128); + private static readonly SKColor DialogBackground = SKColors.White; + private static readonly SKColor TitleColor = new SKColor(0x21, 0x21, 0x21); + private static readonly SKColor MessageColor = new SKColor(0x61, 0x61, 0x61); + private static readonly SKColor ButtonColor = new SKColor(0x21, 0x96, 0xF3); + private static readonly SKColor ButtonHoverColor = new SKColor(0x19, 0x76, 0xD2); + private static readonly SKColor ButtonTextColor = SKColors.White; + private static readonly SKColor CancelButtonColor = new SKColor(0x9E, 0x9E, 0x9E); + private static readonly SKColor CancelButtonHoverColor = new SKColor(0x75, 0x75, 0x75); + private static readonly SKColor BorderColor = new SKColor(0xE0, 0xE0, 0xE0); - private readonly string? _accept; + private const float DialogWidth = 400; + private const float DialogPadding = 24; + private const float ButtonHeight = 44; + private const float ButtonSpacing = 12; + private const float CornerRadius = 12; - private readonly TaskCompletionSource _tcs; + /// + /// Creates a new alert dialog. + /// + public SkiaAlertDialog(string title, string message, string? accept, string? cancel) + { + _title = title; + _message = message; + _accept = accept; + _cancel = cancel; + _tcs = new TaskCompletionSource(); + IsFocusable = true; + } - private SKRect _cancelButtonBounds; + /// + /// Gets the task that completes when the dialog is dismissed. + /// Returns true if accept was clicked, false if cancel was clicked. + /// + public Task Result => _tcs.Task; - private SKRect _acceptButtonBounds; + protected override void OnDraw(SKCanvas canvas, SKRect bounds) + { + // Draw semi-transparent overlay covering entire screen + using var overlayPaint = new SKPaint + { + Color = OverlayColor, + Style = SKPaintStyle.Fill + }; + canvas.DrawRect(bounds, overlayPaint); - private bool _cancelHovered; + // Calculate dialog dimensions + var messageLines = WrapText(_message, DialogWidth - DialogPadding * 2, 16); + var dialogHeight = CalculateDialogHeight(messageLines.Count); - private bool _acceptHovered; + var dialogLeft = bounds.MidX - DialogWidth / 2; + var dialogTop = bounds.MidY - dialogHeight / 2; + var dialogBounds = new SKRect(dialogLeft, dialogTop, dialogLeft + DialogWidth, dialogTop + dialogHeight); - private static readonly SKColor OverlayColor = new SKColor((byte)0, (byte)0, (byte)0, (byte)128); + // Draw dialog shadow + using var shadowPaint = new SKPaint + { + Color = new SKColor(0, 0, 0, 60), + MaskFilter = SKMaskFilter.CreateBlur(SKBlurStyle.Normal, 8), + Style = SKPaintStyle.Fill + }; + var shadowRect = new SKRect(dialogBounds.Left + 4, dialogBounds.Top + 4, + dialogBounds.Right + 4, dialogBounds.Bottom + 4); + canvas.DrawRoundRect(shadowRect, CornerRadius, CornerRadius, shadowPaint); - private static readonly SKColor DialogBackground = SKColors.White; + // Draw dialog background + using var bgPaint = new SKPaint + { + Color = DialogBackground, + Style = SKPaintStyle.Fill, + IsAntialias = true + }; + canvas.DrawRoundRect(dialogBounds, CornerRadius, CornerRadius, bgPaint); - private static readonly SKColor TitleColor = new SKColor((byte)33, (byte)33, (byte)33); + // Draw title + var yOffset = dialogBounds.Top + DialogPadding; + if (!string.IsNullOrEmpty(_title)) + { + using var titleFont = new SKFont(SKTypeface.Default, 20) { Embolden = true }; + using var titlePaint = new SKPaint(titleFont) + { + Color = TitleColor, + IsAntialias = true + }; + canvas.DrawText(_title, dialogBounds.Left + DialogPadding, yOffset + 20, titlePaint); + yOffset += 36; + } - private static readonly SKColor MessageColor = new SKColor((byte)97, (byte)97, (byte)97); + // Draw message + if (!string.IsNullOrEmpty(_message)) + { + using var messageFont = new SKFont(SKTypeface.Default, 16); + using var messagePaint = new SKPaint(messageFont) + { + Color = MessageColor, + IsAntialias = true + }; - private static readonly SKColor ButtonColor = new SKColor((byte)33, (byte)150, (byte)243); + foreach (var line in messageLines) + { + canvas.DrawText(line, dialogBounds.Left + DialogPadding, yOffset + 16, messagePaint); + yOffset += 22; + } + yOffset += 8; + } - private static readonly SKColor ButtonHoverColor = new SKColor((byte)25, (byte)118, (byte)210); + // Draw buttons + yOffset = dialogBounds.Bottom - DialogPadding - ButtonHeight; + var buttonY = yOffset; - private static readonly SKColor ButtonTextColor = SKColors.White; + var buttonCount = (_accept != null ? 1 : 0) + (_cancel != null ? 1 : 0); + var totalButtonWidth = DialogWidth - DialogPadding * 2; - private static readonly SKColor CancelButtonColor = new SKColor((byte)158, (byte)158, (byte)158); + if (buttonCount == 2) + { + var singleButtonWidth = (totalButtonWidth - ButtonSpacing) / 2; - private static readonly SKColor CancelButtonHoverColor = new SKColor((byte)117, (byte)117, (byte)117); + // Cancel button (left) + _cancelButtonBounds = new SKRect( + dialogBounds.Left + DialogPadding, + buttonY, + dialogBounds.Left + DialogPadding + singleButtonWidth, + buttonY + ButtonHeight); + DrawButton(canvas, _cancelButtonBounds, _cancel!, + _cancelHovered ? CancelButtonHoverColor : CancelButtonColor); - private static readonly SKColor BorderColor = new SKColor((byte)224, (byte)224, (byte)224); + // Accept button (right) + _acceptButtonBounds = new SKRect( + dialogBounds.Right - DialogPadding - singleButtonWidth, + buttonY, + dialogBounds.Right - DialogPadding, + buttonY + ButtonHeight); + DrawButton(canvas, _acceptButtonBounds, _accept!, + _acceptHovered ? ButtonHoverColor : ButtonColor); + } + else if (_accept != null) + { + _acceptButtonBounds = new SKRect( + dialogBounds.Left + DialogPadding, + buttonY, + dialogBounds.Right - DialogPadding, + buttonY + ButtonHeight); + DrawButton(canvas, _acceptButtonBounds, _accept, + _acceptHovered ? ButtonHoverColor : ButtonColor); + } + else if (_cancel != null) + { + _cancelButtonBounds = new SKRect( + dialogBounds.Left + DialogPadding, + buttonY, + dialogBounds.Right - DialogPadding, + buttonY + ButtonHeight); + DrawButton(canvas, _cancelButtonBounds, _cancel, + _cancelHovered ? CancelButtonHoverColor : CancelButtonColor); + } + } - private const float DialogWidth = 400f; + private void DrawButton(SKCanvas canvas, SKRect bounds, string text, SKColor bgColor) + { + // Button background + using var bgPaint = new SKPaint + { + Color = bgColor, + Style = SKPaintStyle.Fill, + IsAntialias = true + }; + canvas.DrawRoundRect(bounds, 8, 8, bgPaint); - private const float DialogPadding = 24f; + // Button text + using var font = new SKFont(SKTypeface.Default, 16) { Embolden = true }; + using var textPaint = new SKPaint(font) + { + Color = ButtonTextColor, + IsAntialias = true + }; - private const float ButtonHeight = 44f; + var textBounds = new SKRect(); + textPaint.MeasureText(text, ref textBounds); - private const float ButtonSpacing = 12f; + var x = bounds.MidX - textBounds.MidX; + var y = bounds.MidY - textBounds.MidY; + canvas.DrawText(text, x, y, textPaint); + } - private const float CornerRadius = 12f; + private float CalculateDialogHeight(int messageLineCount) + { + var height = DialogPadding * 2; // Top and bottom padding - public Task Result => _tcs.Task; + if (!string.IsNullOrEmpty(_title)) + height += 36; // Title height - public SkiaAlertDialog(string title, string message, string? accept, string? cancel) - { - _title = title; - _message = message; - _accept = accept; - _cancel = cancel; - _tcs = new TaskCompletionSource(); - base.IsFocusable = true; - } + if (!string.IsNullOrEmpty(_message)) + height += messageLineCount * 22 + 8; // Message lines + spacing - protected override void OnDraw(SKCanvas canvas, SKRect bounds) - { - //IL_0000: Unknown result type (might be due to invalid IL or missing references) - //IL_0005: Unknown result type (might be due to invalid IL or missing references) - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - //IL_0010: Unknown result type (might be due to invalid IL or missing references) - //IL_0018: Expected O, but got Unknown - //IL_0019: Unknown result type (might be due to invalid IL or missing references) - //IL_0071: Unknown result type (might be due to invalid IL or missing references) - //IL_0076: Unknown result type (might be due to invalid IL or missing references) - //IL_0078: Unknown result type (might be due to invalid IL or missing references) - //IL_007d: Unknown result type (might be due to invalid IL or missing references) - //IL_0083: Unknown result type (might be due to invalid IL or missing references) - //IL_008d: Unknown result type (might be due to invalid IL or missing references) - //IL_009e: Unknown result type (might be due to invalid IL or missing references) - //IL_00a7: Expected O, but got Unknown - //IL_00db: Unknown result type (might be due to invalid IL or missing references) - //IL_00e0: Unknown result type (might be due to invalid IL or missing references) - //IL_00e3: Unknown result type (might be due to invalid IL or missing references) - //IL_00f6: Unknown result type (might be due to invalid IL or missing references) - //IL_00fb: Unknown result type (might be due to invalid IL or missing references) - //IL_00fc: Unknown result type (might be due to invalid IL or missing references) - //IL_0106: Unknown result type (might be due to invalid IL or missing references) - //IL_010d: Unknown result type (might be due to invalid IL or missing references) - //IL_0116: Expected O, but got Unknown - //IL_0117: Unknown result type (might be due to invalid IL or missing references) - //IL_015d: Unknown result type (might be due to invalid IL or missing references) - //IL_0162: Unknown result type (might be due to invalid IL or missing references) - //IL_016b: Expected O, but got Unknown - //IL_01f1: Unknown result type (might be due to invalid IL or missing references) - //IL_01f8: Expected O, but got Unknown - //IL_016d: Unknown result type (might be due to invalid IL or missing references) - //IL_0172: Unknown result type (might be due to invalid IL or missing references) - //IL_0173: Unknown result type (might be due to invalid IL or missing references) - //IL_017d: Unknown result type (might be due to invalid IL or missing references) - //IL_0186: Expected O, but got Unknown - //IL_02fd: Unknown result type (might be due to invalid IL or missing references) - //IL_0302: Unknown result type (might be due to invalid IL or missing references) - //IL_030a: Unknown result type (might be due to invalid IL or missing references) - //IL_01fa: Unknown result type (might be due to invalid IL or missing references) - //IL_01ff: Unknown result type (might be due to invalid IL or missing references) - //IL_0200: Unknown result type (might be due to invalid IL or missing references) - //IL_020a: Unknown result type (might be due to invalid IL or missing references) - //IL_0213: Expected O, but got Unknown - //IL_03b9: Unknown result type (might be due to invalid IL or missing references) - //IL_03be: Unknown result type (might be due to invalid IL or missing references) - //IL_03c6: Unknown result type (might be due to invalid IL or missing references) - //IL_0324: Unknown result type (might be due to invalid IL or missing references) - //IL_031d: Unknown result type (might be due to invalid IL or missing references) - //IL_041c: Unknown result type (might be due to invalid IL or missing references) - //IL_0421: Unknown result type (might be due to invalid IL or missing references) - //IL_0429: Unknown result type (might be due to invalid IL or missing references) - //IL_03e0: Unknown result type (might be due to invalid IL or missing references) - //IL_03d9: Unknown result type (might be due to invalid IL or missing references) - //IL_0356: Unknown result type (might be due to invalid IL or missing references) - //IL_035b: Unknown result type (might be due to invalid IL or missing references) - //IL_0363: Unknown result type (might be due to invalid IL or missing references) - //IL_0443: Unknown result type (might be due to invalid IL or missing references) - //IL_043c: Unknown result type (might be due to invalid IL or missing references) - //IL_037d: Unknown result type (might be due to invalid IL or missing references) - //IL_0376: Unknown result type (might be due to invalid IL or missing references) - SKPaint val = new SKPaint - { - Color = OverlayColor, - Style = (SKPaintStyle)0 - }; - try - { - canvas.DrawRect(bounds, val); - List list = WrapText(_message, 352f, 16f); - float num = CalculateDialogHeight(list.Count); - float num2 = ((SKRect)(ref bounds)).MidX - 200f; - float num3 = ((SKRect)(ref bounds)).MidY - num / 2f; - SKRect val2 = new SKRect(num2, num3, num2 + 400f, num3 + num); - SKPaint val3 = new SKPaint - { - Color = new SKColor((byte)0, (byte)0, (byte)0, (byte)60), - MaskFilter = SKMaskFilter.CreateBlur((SKBlurStyle)0, 8f), - Style = (SKPaintStyle)0 - }; - try - { - SKRect val4 = new SKRect(((SKRect)(ref val2)).Left + 4f, ((SKRect)(ref val2)).Top + 4f, ((SKRect)(ref val2)).Right + 4f, ((SKRect)(ref val2)).Bottom + 4f); - canvas.DrawRoundRect(val4, 12f, 12f, val3); - SKPaint val5 = new SKPaint - { - Color = DialogBackground, - Style = (SKPaintStyle)0, - IsAntialias = true - }; - try - { - canvas.DrawRoundRect(val2, 12f, 12f, val5); - float num4 = ((SKRect)(ref val2)).Top + 24f; - if (!string.IsNullOrEmpty(_title)) - { - SKFont val6 = new SKFont(SKTypeface.Default, 20f, 1f, 0f) - { - Embolden = true - }; - try - { - SKPaint val7 = new SKPaint(val6) - { - Color = TitleColor, - IsAntialias = true - }; - try - { - canvas.DrawText(_title, ((SKRect)(ref val2)).Left + 24f, num4 + 20f, val7); - num4 += 36f; - } - finally - { - ((IDisposable)val7)?.Dispose(); - } - } - finally - { - ((IDisposable)val6)?.Dispose(); - } - } - if (!string.IsNullOrEmpty(_message)) - { - SKFont val8 = new SKFont(SKTypeface.Default, 16f, 1f, 0f); - try - { - SKPaint val9 = new SKPaint(val8) - { - Color = MessageColor, - IsAntialias = true - }; - try - { - foreach (string item in list) - { - canvas.DrawText(item, ((SKRect)(ref val2)).Left + 24f, num4 + 16f, val9); - num4 += 22f; - } - num4 += 8f; - } - finally - { - ((IDisposable)val9)?.Dispose(); - } - } - finally - { - ((IDisposable)val8)?.Dispose(); - } - } - num4 = ((SKRect)(ref val2)).Bottom - 24f - 44f; - float num5 = num4; - int num6 = ((_accept != null) ? 1 : 0) + ((_cancel != null) ? 1 : 0); - float num7 = 352f; - if (num6 == 2) - { - float num8 = (num7 - 12f) / 2f; - _cancelButtonBounds = new SKRect(((SKRect)(ref val2)).Left + 24f, num5, ((SKRect)(ref val2)).Left + 24f + num8, num5 + 44f); - DrawButton(canvas, _cancelButtonBounds, _cancel, _cancelHovered ? CancelButtonHoverColor : CancelButtonColor); - _acceptButtonBounds = new SKRect(((SKRect)(ref val2)).Right - 24f - num8, num5, ((SKRect)(ref val2)).Right - 24f, num5 + 44f); - DrawButton(canvas, _acceptButtonBounds, _accept, _acceptHovered ? ButtonHoverColor : ButtonColor); - } - else if (_accept != null) - { - _acceptButtonBounds = new SKRect(((SKRect)(ref val2)).Left + 24f, num5, ((SKRect)(ref val2)).Right - 24f, num5 + 44f); - DrawButton(canvas, _acceptButtonBounds, _accept, _acceptHovered ? ButtonHoverColor : ButtonColor); - } - else if (_cancel != null) - { - _cancelButtonBounds = new SKRect(((SKRect)(ref val2)).Left + 24f, num5, ((SKRect)(ref val2)).Right - 24f, num5 + 44f); - DrawButton(canvas, _cancelButtonBounds, _cancel, _cancelHovered ? CancelButtonHoverColor : CancelButtonColor); - } - } - finally - { - ((IDisposable)val5)?.Dispose(); - } - } - finally - { - ((IDisposable)val3)?.Dispose(); - } - } - finally - { - ((IDisposable)val)?.Dispose(); - } - } + height += ButtonHeight; // Buttons - private void DrawButton(SKCanvas canvas, SKRect bounds, string text, SKColor bgColor) - { - //IL_0000: Unknown result type (might be due to invalid IL or missing references) - //IL_0005: Unknown result type (might be due to invalid IL or missing references) - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - //IL_000d: Unknown result type (might be due to invalid IL or missing references) - //IL_0014: Unknown result type (might be due to invalid IL or missing references) - //IL_001c: Expected O, but got Unknown - //IL_001d: Unknown result type (might be due to invalid IL or missing references) - //IL_0042: Unknown result type (might be due to invalid IL or missing references) - //IL_0047: Unknown result type (might be due to invalid IL or missing references) - //IL_004f: Expected O, but got Unknown - //IL_0050: Unknown result type (might be due to invalid IL or missing references) - //IL_0055: Unknown result type (might be due to invalid IL or missing references) - //IL_0056: Unknown result type (might be due to invalid IL or missing references) - //IL_0060: Unknown result type (might be due to invalid IL or missing references) - //IL_0068: Expected O, but got Unknown - //IL_006a: Unknown result type (might be due to invalid IL or missing references) - SKPaint val = new SKPaint - { - Color = bgColor, - Style = (SKPaintStyle)0, - IsAntialias = true - }; - try - { - canvas.DrawRoundRect(bounds, 8f, 8f, val); - SKFont val2 = new SKFont(SKTypeface.Default, 16f, 1f, 0f) - { - Embolden = true - }; - try - { - SKPaint val3 = new SKPaint(val2) - { - Color = ButtonTextColor, - IsAntialias = true - }; - try - { - SKRect val4 = default(SKRect); - val3.MeasureText(text, ref val4); - float num = ((SKRect)(ref bounds)).MidX - ((SKRect)(ref val4)).MidX; - float num2 = ((SKRect)(ref bounds)).MidY - ((SKRect)(ref val4)).MidY; - canvas.DrawText(text, num, num2, val3); - } - finally - { - ((IDisposable)val3)?.Dispose(); - } - } - finally - { - ((IDisposable)val2)?.Dispose(); - } - } - finally - { - ((IDisposable)val)?.Dispose(); - } - } + return Math.Max(height, 180); // Minimum height + } - private float CalculateDialogHeight(int messageLineCount) - { - float num = 48f; - if (!string.IsNullOrEmpty(_title)) - { - num += 36f; - } - if (!string.IsNullOrEmpty(_message)) - { - num += (float)(messageLineCount * 22 + 8); - } - num += 44f; - return Math.Max(num, 180f); - } + private List WrapText(string text, float maxWidth, float fontSize) + { + var lines = new List(); + if (string.IsNullOrEmpty(text)) + return lines; - private List WrapText(string text, float maxWidth, float fontSize) - { - //IL_0020: Unknown result type (might be due to invalid IL or missing references) - //IL_0026: Expected O, but got Unknown - //IL_0027: Unknown result type (might be due to invalid IL or missing references) - //IL_002d: Expected O, but got Unknown - List list = new List(); - if (string.IsNullOrEmpty(text)) - { - return list; - } - SKFont val = new SKFont(SKTypeface.Default, fontSize, 1f, 0f); - try - { - SKPaint val2 = new SKPaint(val); - try - { - string[] array = text.Split(' '); - string text2 = ""; - string[] array2 = array; - foreach (string text3 in array2) - { - string text4 = (string.IsNullOrEmpty(text2) ? text3 : (text2 + " " + text3)); - if (val2.MeasureText(text4) > maxWidth && !string.IsNullOrEmpty(text2)) - { - list.Add(text2); - text2 = text3; - } - else - { - text2 = text4; - } - } - if (!string.IsNullOrEmpty(text2)) - { - list.Add(text2); - } - return list; - } - finally - { - ((IDisposable)val2)?.Dispose(); - } - } - finally - { - ((IDisposable)val)?.Dispose(); - } - } + using var font = new SKFont(SKTypeface.Default, fontSize); + using var paint = new SKPaint(font); - public override void OnPointerMoved(PointerEventArgs e) - { - bool num = _cancelHovered || _acceptHovered; - _cancelHovered = _cancel != null && ((SKRect)(ref _cancelButtonBounds)).Contains(e.X, e.Y); - _acceptHovered = _accept != null && ((SKRect)(ref _acceptButtonBounds)).Contains(e.X, e.Y); - if (num != (_cancelHovered || _acceptHovered)) - { - Invalidate(); - } - } + var words = text.Split(' '); + var currentLine = ""; - public override void OnPointerPressed(PointerEventArgs e) - { - if (_cancel != null && ((SKRect)(ref _cancelButtonBounds)).Contains(e.X, e.Y)) - { - Dismiss(result: false); - } - else if (_accept != null && ((SKRect)(ref _acceptButtonBounds)).Contains(e.X, e.Y)) - { - Dismiss(result: true); - } - } + foreach (var word in words) + { + var testLine = string.IsNullOrEmpty(currentLine) ? word : currentLine + " " + word; + var width = paint.MeasureText(testLine); - public override void OnKeyDown(KeyEventArgs e) - { - if (e.Key == Key.Escape && _cancel != null) - { - Dismiss(result: false); - e.Handled = true; - } - else if (e.Key == Key.Enter && _accept != null) - { - Dismiss(result: true); - e.Handled = true; - } - } + if (width > maxWidth && !string.IsNullOrEmpty(currentLine)) + { + lines.Add(currentLine); + currentLine = word; + } + else + { + currentLine = testLine; + } + } - private void Dismiss(bool result) - { - LinuxDialogService.HideDialog(this); - _tcs.TrySetResult(result); - } + if (!string.IsNullOrEmpty(currentLine)) + lines.Add(currentLine); - protected override SKSize MeasureOverride(SKSize availableSize) - { - //IL_0000: Unknown result type (might be due to invalid IL or missing references) - return availableSize; - } + return lines; + } - public override SkiaView? HitTest(float x, float y) - { - return this; - } + public override void OnPointerMoved(PointerEventArgs e) + { + var wasHovered = _cancelHovered || _acceptHovered; + + _cancelHovered = _cancel != null && _cancelButtonBounds.Contains(e.X, e.Y); + _acceptHovered = _accept != null && _acceptButtonBounds.Contains(e.X, e.Y); + + if (wasHovered != (_cancelHovered || _acceptHovered)) + Invalidate(); + } + + public override void OnPointerPressed(PointerEventArgs e) + { + // Check if clicking on buttons + if (_cancel != null && _cancelButtonBounds.Contains(e.X, e.Y)) + { + Dismiss(false); + return; + } + + if (_accept != null && _acceptButtonBounds.Contains(e.X, e.Y)) + { + Dismiss(true); + return; + } + + // Clicking outside dialog doesn't dismiss it (it's modal) + } + + public override void OnKeyDown(KeyEventArgs e) + { + // Handle Escape to cancel + if (e.Key == Key.Escape && _cancel != null) + { + Dismiss(false); + e.Handled = true; + return; + } + + // Handle Enter to accept + if (e.Key == Key.Enter && _accept != null) + { + Dismiss(true); + e.Handled = true; + return; + } + } + + private void Dismiss(bool result) + { + // Remove from dialog system + LinuxDialogService.HideDialog(this); + _tcs.TrySetResult(result); + } + + protected override SKSize MeasureOverride(SKSize availableSize) + { + // Dialog takes full screen for the overlay + return availableSize; + } + + public override SkiaView? HitTest(float x, float y) + { + // Modal dialogs capture all input + return this; + } +} + +/// +/// Service for showing modal dialogs in OpenMaui Linux. +/// +public static class LinuxDialogService +{ + private static readonly List _activeDialogs = new(); + private static Action? _invalidateCallback; + + /// + /// Registers the invalidation callback (called by LinuxApplication). + /// + public static void SetInvalidateCallback(Action callback) + { + _invalidateCallback = callback; + } + + /// + /// Shows an alert dialog and returns when dismissed. + /// + public static Task ShowAlertAsync(string title, string message, string? accept, string? cancel) + { + var dialog = new SkiaAlertDialog(title, message, accept, cancel); + _activeDialogs.Add(dialog); + _invalidateCallback?.Invoke(); + return dialog.Result; + } + + /// + /// Hides a dialog. + /// + internal static void HideDialog(SkiaAlertDialog dialog) + { + _activeDialogs.Remove(dialog); + _invalidateCallback?.Invoke(); + } + + /// + /// Gets whether there are active dialogs. + /// + public static bool HasActiveDialog => _activeDialogs.Count > 0; + + /// + /// Gets the topmost dialog. + /// + public static SkiaAlertDialog? TopDialog => _activeDialogs.Count > 0 ? _activeDialogs[^1] : null; + + /// + /// Draws all active dialogs. + /// + public static void DrawDialogs(SKCanvas canvas, SKRect bounds) + { + foreach (var dialog in _activeDialogs) + { + dialog.Measure(new SKSize(bounds.Width, bounds.Height)); + dialog.Arrange(bounds); + dialog.Draw(canvas); + } + } } diff --git a/Views/SkiaBorder.cs b/Views/SkiaBorder.cs index a39a181..79b5268 100644 --- a/Views/SkiaBorder.cs +++ b/Views/SkiaBorder.cs @@ -1,483 +1,310 @@ -using System; -using Microsoft.Maui.Controls; -using Microsoft.Maui.Platform.Linux.Handlers; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using SkiaSharp; namespace Microsoft.Maui.Platform; +/// +/// Skia-rendered border/frame container control with full XAML styling support. +/// public class SkiaBorder : SkiaLayoutView { - public static readonly BindableProperty StrokeThicknessProperty = BindableProperty.Create("StrokeThickness", typeof(float), typeof(SkiaBorder), (object)1f, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaBorder)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + #region BindableProperties - public static readonly BindableProperty CornerRadiusProperty = BindableProperty.Create("CornerRadius", typeof(float), typeof(SkiaBorder), (object)0f, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaBorder)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + public static readonly BindableProperty StrokeThicknessProperty = + BindableProperty.Create(nameof(StrokeThickness), typeof(float), typeof(SkiaBorder), 1f, + propertyChanged: (b, o, n) => ((SkiaBorder)b).Invalidate()); - public static readonly BindableProperty StrokeProperty = BindableProperty.Create("Stroke", typeof(SKColor), typeof(SkiaBorder), (object)SKColors.Black, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaBorder)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + public static readonly BindableProperty CornerRadiusProperty = + BindableProperty.Create(nameof(CornerRadius), typeof(float), typeof(SkiaBorder), 0f, + propertyChanged: (b, o, n) => ((SkiaBorder)b).Invalidate()); - public static readonly BindableProperty PaddingLeftProperty = BindableProperty.Create("PaddingLeft", typeof(float), typeof(SkiaBorder), (object)0f, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaBorder)(object)b).InvalidateMeasure(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + public static readonly BindableProperty StrokeProperty = + BindableProperty.Create(nameof(Stroke), typeof(SKColor), typeof(SkiaBorder), SKColors.Black, + propertyChanged: (b, o, n) => ((SkiaBorder)b).Invalidate()); - public static readonly BindableProperty PaddingTopProperty = BindableProperty.Create("PaddingTop", typeof(float), typeof(SkiaBorder), (object)0f, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaBorder)(object)b).InvalidateMeasure(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + public static readonly BindableProperty PaddingLeftProperty = + BindableProperty.Create(nameof(PaddingLeft), typeof(float), typeof(SkiaBorder), 0f, + propertyChanged: (b, o, n) => ((SkiaBorder)b).InvalidateMeasure()); - public static readonly BindableProperty PaddingRightProperty = BindableProperty.Create("PaddingRight", typeof(float), typeof(SkiaBorder), (object)0f, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaBorder)(object)b).InvalidateMeasure(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + public static readonly BindableProperty PaddingTopProperty = + BindableProperty.Create(nameof(PaddingTop), typeof(float), typeof(SkiaBorder), 0f, + propertyChanged: (b, o, n) => ((SkiaBorder)b).InvalidateMeasure()); - public static readonly BindableProperty PaddingBottomProperty = BindableProperty.Create("PaddingBottom", typeof(float), typeof(SkiaBorder), (object)0f, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaBorder)(object)b).InvalidateMeasure(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + public static readonly BindableProperty PaddingRightProperty = + BindableProperty.Create(nameof(PaddingRight), typeof(float), typeof(SkiaBorder), 0f, + propertyChanged: (b, o, n) => ((SkiaBorder)b).InvalidateMeasure()); - public static readonly BindableProperty HasShadowProperty = BindableProperty.Create("HasShadow", typeof(bool), typeof(SkiaBorder), (object)false, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaBorder)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + public static readonly BindableProperty PaddingBottomProperty = + BindableProperty.Create(nameof(PaddingBottom), typeof(float), typeof(SkiaBorder), 0f, + propertyChanged: (b, o, n) => ((SkiaBorder)b).InvalidateMeasure()); - public static readonly BindableProperty ShadowColorProperty = BindableProperty.Create("ShadowColor", typeof(SKColor), typeof(SkiaBorder), (object)new SKColor((byte)0, (byte)0, (byte)0, (byte)40), (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaBorder)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + public static readonly BindableProperty HasShadowProperty = + BindableProperty.Create(nameof(HasShadow), typeof(bool), typeof(SkiaBorder), false, + propertyChanged: (b, o, n) => ((SkiaBorder)b).Invalidate()); - public static readonly BindableProperty ShadowBlurRadiusProperty = BindableProperty.Create("ShadowBlurRadius", typeof(float), typeof(SkiaBorder), (object)4f, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaBorder)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + public static readonly BindableProperty ShadowColorProperty = + BindableProperty.Create(nameof(ShadowColor), typeof(SKColor), typeof(SkiaBorder), new SKColor(0, 0, 0, 40), + propertyChanged: (b, o, n) => ((SkiaBorder)b).Invalidate()); - public static readonly BindableProperty ShadowOffsetXProperty = BindableProperty.Create("ShadowOffsetX", typeof(float), typeof(SkiaBorder), (object)2f, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaBorder)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + public static readonly BindableProperty ShadowBlurRadiusProperty = + BindableProperty.Create(nameof(ShadowBlurRadius), typeof(float), typeof(SkiaBorder), 4f, + propertyChanged: (b, o, n) => ((SkiaBorder)b).Invalidate()); - public static readonly BindableProperty ShadowOffsetYProperty = BindableProperty.Create("ShadowOffsetY", typeof(float), typeof(SkiaBorder), (object)2f, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaBorder)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + public static readonly BindableProperty ShadowOffsetXProperty = + BindableProperty.Create(nameof(ShadowOffsetX), typeof(float), typeof(SkiaBorder), 2f, + propertyChanged: (b, o, n) => ((SkiaBorder)b).Invalidate()); - private bool _isPressed; + public static readonly BindableProperty ShadowOffsetYProperty = + BindableProperty.Create(nameof(ShadowOffsetY), typeof(float), typeof(SkiaBorder), 2f, + propertyChanged: (b, o, n) => ((SkiaBorder)b).Invalidate()); - public float StrokeThickness - { - get - { - return (float)((BindableObject)this).GetValue(StrokeThicknessProperty); - } - set - { - ((BindableObject)this).SetValue(StrokeThicknessProperty, (object)value); - } - } + #endregion - public float CornerRadius - { - get - { - return (float)((BindableObject)this).GetValue(CornerRadiusProperty); - } - set - { - ((BindableObject)this).SetValue(CornerRadiusProperty, (object)value); - } - } + #region Properties - public SKColor Stroke - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKColor)((BindableObject)this).GetValue(StrokeProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(StrokeProperty, (object)value); - } - } + public float StrokeThickness + { + get => (float)GetValue(StrokeThicknessProperty); + set => SetValue(StrokeThicknessProperty, value); + } - public float PaddingLeft - { - get - { - return (float)((BindableObject)this).GetValue(PaddingLeftProperty); - } - set - { - ((BindableObject)this).SetValue(PaddingLeftProperty, (object)value); - } - } + public float CornerRadius + { + get => (float)GetValue(CornerRadiusProperty); + set => SetValue(CornerRadiusProperty, value); + } - public float PaddingTop - { - get - { - return (float)((BindableObject)this).GetValue(PaddingTopProperty); - } - set - { - ((BindableObject)this).SetValue(PaddingTopProperty, (object)value); - } - } + public SKColor Stroke + { + get => (SKColor)GetValue(StrokeProperty); + set => SetValue(StrokeProperty, value); + } - public float PaddingRight - { - get - { - return (float)((BindableObject)this).GetValue(PaddingRightProperty); - } - set - { - ((BindableObject)this).SetValue(PaddingRightProperty, (object)value); - } - } + public float PaddingLeft + { + get => (float)GetValue(PaddingLeftProperty); + set => SetValue(PaddingLeftProperty, value); + } - public float PaddingBottom - { - get - { - return (float)((BindableObject)this).GetValue(PaddingBottomProperty); - } - set - { - ((BindableObject)this).SetValue(PaddingBottomProperty, (object)value); - } - } + public float PaddingTop + { + get => (float)GetValue(PaddingTopProperty); + set => SetValue(PaddingTopProperty, value); + } - public bool HasShadow - { - get - { - return (bool)((BindableObject)this).GetValue(HasShadowProperty); - } - set - { - ((BindableObject)this).SetValue(HasShadowProperty, (object)value); - } - } + public float PaddingRight + { + get => (float)GetValue(PaddingRightProperty); + set => SetValue(PaddingRightProperty, value); + } - public SKColor ShadowColor - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKColor)((BindableObject)this).GetValue(ShadowColorProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(ShadowColorProperty, (object)value); - } - } + public float PaddingBottom + { + get => (float)GetValue(PaddingBottomProperty); + set => SetValue(PaddingBottomProperty, value); + } - public float ShadowBlurRadius - { - get - { - return (float)((BindableObject)this).GetValue(ShadowBlurRadiusProperty); - } - set - { - ((BindableObject)this).SetValue(ShadowBlurRadiusProperty, (object)value); - } - } + public bool HasShadow + { + get => (bool)GetValue(HasShadowProperty); + set => SetValue(HasShadowProperty, value); + } - public float ShadowOffsetX - { - get - { - return (float)((BindableObject)this).GetValue(ShadowOffsetXProperty); - } - set - { - ((BindableObject)this).SetValue(ShadowOffsetXProperty, (object)value); - } - } + public SKColor ShadowColor + { + get => (SKColor)GetValue(ShadowColorProperty); + set => SetValue(ShadowColorProperty, value); + } - public float ShadowOffsetY - { - get - { - return (float)((BindableObject)this).GetValue(ShadowOffsetYProperty); - } - set - { - ((BindableObject)this).SetValue(ShadowOffsetYProperty, (object)value); - } - } + public float ShadowBlurRadius + { + get => (float)GetValue(ShadowBlurRadiusProperty); + set => SetValue(ShadowBlurRadiusProperty, value); + } - public event EventHandler? Tapped; + public float ShadowOffsetX + { + get => (float)GetValue(ShadowOffsetXProperty); + set => SetValue(ShadowOffsetXProperty, value); + } - public void SetPadding(float all) - { - float num = (PaddingBottom = all); - float num3 = (PaddingRight = num); - float paddingLeft = (PaddingTop = num3); - PaddingLeft = paddingLeft; - } + public float ShadowOffsetY + { + get => (float)GetValue(ShadowOffsetYProperty); + set => SetValue(ShadowOffsetYProperty, value); + } - public void SetPadding(float horizontal, float vertical) - { - float paddingLeft = (PaddingRight = horizontal); - PaddingLeft = paddingLeft; - paddingLeft = (PaddingBottom = vertical); - PaddingTop = paddingLeft; - } + #endregion - public void SetPadding(float left, float top, float right, float bottom) - { - PaddingLeft = left; - PaddingTop = top; - PaddingRight = right; - PaddingBottom = bottom; - } + /// + /// Sets uniform padding on all sides. + /// + public void SetPadding(float all) + { + PaddingLeft = PaddingTop = PaddingRight = PaddingBottom = all; + } - protected override void OnDraw(SKCanvas canvas, SKRect bounds) - { - //IL_00e5: Unknown result type (might be due to invalid IL or missing references) - //IL_00ea: Unknown result type (might be due to invalid IL or missing references) - //IL_00ec: Unknown result type (might be due to invalid IL or missing references) - //IL_00f6: Unknown result type (might be due to invalid IL or missing references) - //IL_00fd: Unknown result type (might be due to invalid IL or missing references) - //IL_0105: Expected O, but got Unknown - //IL_005c: Unknown result type (might be due to invalid IL or missing references) - //IL_0061: Unknown result type (might be due to invalid IL or missing references) - //IL_0063: Unknown result type (might be due to invalid IL or missing references) - //IL_006d: Unknown result type (might be due to invalid IL or missing references) - //IL_007f: Unknown result type (might be due to invalid IL or missing references) - //IL_0088: Expected O, but got Unknown - //IL_0106: Unknown result type (might be due to invalid IL or missing references) - //IL_0108: Unknown result type (might be due to invalid IL or missing references) - //IL_0113: Expected O, but got Unknown - //IL_00c0: Unknown result type (might be due to invalid IL or missing references) - //IL_00c5: Unknown result type (might be due to invalid IL or missing references) - //IL_00c8: Unknown result type (might be due to invalid IL or missing references) - //IL_00cb: Unknown result type (might be due to invalid IL or missing references) - //IL_00d7: Expected O, but got Unknown - //IL_011b: Unknown result type (might be due to invalid IL or missing references) - //IL_0120: Unknown result type (might be due to invalid IL or missing references) - //IL_0122: Unknown result type (might be due to invalid IL or missing references) - //IL_012c: Unknown result type (might be due to invalid IL or missing references) - //IL_0133: Unknown result type (might be due to invalid IL or missing references) - //IL_013a: Unknown result type (might be due to invalid IL or missing references) - //IL_0143: Expected O, but got Unknown - //IL_0144: Unknown result type (might be due to invalid IL or missing references) - //IL_0146: Unknown result type (might be due to invalid IL or missing references) - //IL_0152: Expected O, but got Unknown - float strokeThickness = StrokeThickness; - float cornerRadius = CornerRadius; - SKRect val = default(SKRect); - ((SKRect)(ref val))._002Ector(((SKRect)(ref bounds)).Left + strokeThickness / 2f, ((SKRect)(ref bounds)).Top + strokeThickness / 2f, ((SKRect)(ref bounds)).Right - strokeThickness / 2f, ((SKRect)(ref bounds)).Bottom - strokeThickness / 2f); - if (HasShadow) - { - SKPaint val2 = new SKPaint - { - Color = ShadowColor, - MaskFilter = SKMaskFilter.CreateBlur((SKBlurStyle)0, ShadowBlurRadius), - Style = (SKPaintStyle)0 - }; - try - { - SKRect val3 = new SKRect(((SKRect)(ref val)).Left + ShadowOffsetX, ((SKRect)(ref val)).Top + ShadowOffsetY, ((SKRect)(ref val)).Right + ShadowOffsetX, ((SKRect)(ref val)).Bottom + ShadowOffsetY); - canvas.DrawRoundRect(new SKRoundRect(val3, cornerRadius), val2); - } - finally - { - ((IDisposable)val2)?.Dispose(); - } - } - SKPaint val4 = new SKPaint - { - Color = base.BackgroundColor, - Style = (SKPaintStyle)0, - IsAntialias = true - }; - try - { - canvas.DrawRoundRect(new SKRoundRect(val, cornerRadius), val4); - if (strokeThickness > 0f) - { - SKPaint val5 = new SKPaint - { - Color = Stroke, - Style = (SKPaintStyle)1, - StrokeWidth = strokeThickness, - IsAntialias = true - }; - try - { - canvas.DrawRoundRect(new SKRoundRect(val, cornerRadius), val5); - } - finally - { - ((IDisposable)val5)?.Dispose(); - } - } - foreach (SkiaView child in base.Children) - { - if (child.IsVisible) - { - child.Draw(canvas); - } - } - } - finally - { - ((IDisposable)val4)?.Dispose(); - } - } + /// + /// Sets padding with horizontal and vertical values. + /// + public void SetPadding(float horizontal, float vertical) + { + PaddingLeft = PaddingRight = horizontal; + PaddingTop = PaddingBottom = vertical; + } - protected override SKRect GetContentBounds() - { - //IL_0002: Unknown result type (might be due to invalid IL or missing references) - //IL_0007: Unknown result type (might be due to invalid IL or missing references) - return GetContentBounds(base.Bounds); - } + /// + /// Sets padding with individual values for each side. + /// + public void SetPadding(float left, float top, float right, float bottom) + { + PaddingLeft = left; + PaddingTop = top; + PaddingRight = right; + PaddingBottom = bottom; + } - protected new SKRect GetContentBounds(SKRect bounds) - { - //IL_0047: Unknown result type (might be due to invalid IL or missing references) - float strokeThickness = StrokeThickness; - return new SKRect(((SKRect)(ref bounds)).Left + PaddingLeft + strokeThickness, ((SKRect)(ref bounds)).Top + PaddingTop + strokeThickness, ((SKRect)(ref bounds)).Right - PaddingRight - strokeThickness, ((SKRect)(ref bounds)).Bottom - PaddingBottom - strokeThickness); - } + protected override void OnDraw(SKCanvas canvas, SKRect bounds) + { + var strokeThickness = StrokeThickness; + var cornerRadius = CornerRadius; - protected override SKSize MeasureOverride(SKSize availableSize) - { - //IL_009a: Unknown result type (might be due to invalid IL or missing references) - //IL_009f: Unknown result type (might be due to invalid IL or missing references) - //IL_00b7: Unknown result type (might be due to invalid IL or missing references) - //IL_00b9: Unknown result type (might be due to invalid IL or missing references) - //IL_00be: Unknown result type (might be due to invalid IL or missing references) - //IL_00e6: Unknown result type (might be due to invalid IL or missing references) - //IL_00eb: Unknown result type (might be due to invalid IL or missing references) - //IL_014e: Unknown result type (might be due to invalid IL or missing references) - float strokeThickness = StrokeThickness; - float num = PaddingLeft + PaddingRight + strokeThickness * 2f; - float num2 = PaddingTop + PaddingBottom + strokeThickness * 2f; - float num3 = ((base.WidthRequest >= 0.0) ? ((float)base.WidthRequest) : ((SKSize)(ref availableSize)).Width); - float num4 = ((base.HeightRequest >= 0.0) ? ((float)base.HeightRequest) : ((SKSize)(ref availableSize)).Height); - SKSize availableSize2 = default(SKSize); - ((SKSize)(ref availableSize2))._002Ector(Math.Max(0f, num3 - num), Math.Max(0f, num4 - num2)); - SKSize val = SKSize.Empty; - foreach (SkiaView child in base.Children) - { - SKSize val2 = child.Measure(availableSize2); - val = new SKSize(Math.Max(((SKSize)(ref val)).Width, ((SKSize)(ref val2)).Width), Math.Max(((SKSize)(ref val)).Height, ((SKSize)(ref val2)).Height)); - } - float num5 = ((base.WidthRequest >= 0.0) ? ((float)base.WidthRequest) : (((SKSize)(ref val)).Width + num)); - float num6 = ((base.HeightRequest >= 0.0) ? ((float)base.HeightRequest) : (((SKSize)(ref val)).Height + num2)); - return new SKSize(num5, num6); - } + var borderRect = new SKRect( + bounds.Left + strokeThickness / 2, + bounds.Top + strokeThickness / 2, + bounds.Right - strokeThickness / 2, + bounds.Bottom - strokeThickness / 2); - protected override SKRect ArrangeOverride(SKRect bounds) - { - //IL_0001: Unknown result type (might be due to invalid IL or missing references) - //IL_0002: Unknown result type (might be due to invalid IL or missing references) - //IL_0007: Unknown result type (might be due to invalid IL or missing references) - //IL_001d: Unknown result type (might be due to invalid IL or missing references) - //IL_0022: Unknown result type (might be due to invalid IL or missing references) - //IL_006a: Unknown result type (might be due to invalid IL or missing references) - //IL_0084: Unknown result type (might be due to invalid IL or missing references) - SKRect contentBounds = GetContentBounds(bounds); - SKRect bounds2 = default(SKRect); - foreach (SkiaView child in base.Children) - { - Thickness margin = child.Margin; - ((SKRect)(ref bounds2))._002Ector(((SKRect)(ref contentBounds)).Left + (float)((Thickness)(ref margin)).Left, ((SKRect)(ref contentBounds)).Top + (float)((Thickness)(ref margin)).Top, ((SKRect)(ref contentBounds)).Right - (float)((Thickness)(ref margin)).Right, ((SKRect)(ref contentBounds)).Bottom - (float)((Thickness)(ref margin)).Bottom); - child.Arrange(bounds2); - } - return bounds; - } + // Draw shadow if enabled + if (HasShadow) + { + using var shadowPaint = new SKPaint + { + Color = ShadowColor, + MaskFilter = SKMaskFilter.CreateBlur(SKBlurStyle.Normal, ShadowBlurRadius), + Style = SKPaintStyle.Fill + }; + var shadowRect = new SKRect( + borderRect.Left + ShadowOffsetX, + borderRect.Top + ShadowOffsetY, + borderRect.Right + ShadowOffsetX, + borderRect.Bottom + ShadowOffsetY); + canvas.DrawRoundRect(new SKRoundRect(shadowRect, cornerRadius), shadowPaint); + } - private bool HasTapGestureRecognizers() - { - View? mauiView = base.MauiView; - if (((mauiView != null) ? mauiView.GestureRecognizers : null) == null) - { - return false; - } - foreach (IGestureRecognizer gestureRecognizer in base.MauiView.GestureRecognizers) - { - if (gestureRecognizer is TapGestureRecognizer) - { - return true; - } - } - return false; - } + // Draw background + using var bgPaint = new SKPaint + { + Color = BackgroundColor, + Style = SKPaintStyle.Fill, + IsAntialias = true + }; + canvas.DrawRoundRect(new SKRoundRect(borderRect, cornerRadius), bgPaint); - public override SkiaView? HitTest(float x, float y) - { - //IL_0011: Unknown result type (might be due to invalid IL or missing references) - //IL_0016: Unknown result type (might be due to invalid IL or missing references) - //IL_001b: Unknown result type (might be due to invalid IL or missing references) - if (base.IsVisible && base.IsEnabled) - { - SKRect bounds = base.Bounds; - if (((SKRect)(ref bounds)).Contains(new SKPoint(x, y))) - { - if (HasTapGestureRecognizers()) - { - Console.WriteLine("[SkiaBorder.HitTest] Intercepting for gesture - returning self"); - return this; - } - return base.HitTest(x, y); - } - } - return null; - } + // Draw border + if (strokeThickness > 0) + { + using var borderPaint = new SKPaint + { + Color = Stroke, + Style = SKPaintStyle.Stroke, + StrokeWidth = strokeThickness, + IsAntialias = true + }; + canvas.DrawRoundRect(new SKRoundRect(borderRect, cornerRadius), borderPaint); + } - public override void OnPointerPressed(PointerEventArgs e) - { - if (HasTapGestureRecognizers()) - { - _isPressed = true; - e.Handled = true; - Console.WriteLine("[SkiaBorder] OnPointerPressed INTERCEPTED for gesture, MauiView=" + ((object)base.MauiView)?.GetType().Name); - if (base.MauiView != null) - { - GestureManager.ProcessPointerDown(base.MauiView, e.X, e.Y); - } - } - else - { - base.OnPointerPressed(e); - } - } + // Draw children + foreach (var child in Children) + { + if (child.IsVisible) + { + child.Draw(canvas); + } + } + } - public override void OnPointerReleased(PointerEventArgs e) - { - if (_isPressed) - { - _isPressed = false; - e.Handled = true; - Console.WriteLine("[SkiaBorder] OnPointerReleased - processing gesture recognizers, MauiView=" + ((object)base.MauiView)?.GetType().Name); - if (base.MauiView != null) - { - GestureManager.ProcessPointerUp(base.MauiView, e.X, e.Y); - } - this.Tapped?.Invoke(this, EventArgs.Empty); - } - else - { - base.OnPointerReleased(e); - } - } + protected override SKRect GetContentBounds() + { + return GetContentBounds(Bounds); + } - public override void OnPointerExited(PointerEventArgs e) - { - base.OnPointerExited(e); - _isPressed = false; - } + protected new SKRect GetContentBounds(SKRect bounds) + { + var strokeThickness = StrokeThickness; + return new SKRect( + bounds.Left + PaddingLeft + strokeThickness, + bounds.Top + PaddingTop + strokeThickness, + bounds.Right - PaddingRight - strokeThickness, + bounds.Bottom - PaddingBottom - strokeThickness); + } + + protected override SKSize MeasureOverride(SKSize availableSize) + { + var strokeThickness = StrokeThickness; + var paddingWidth = PaddingLeft + PaddingRight + strokeThickness * 2; + var paddingHeight = PaddingTop + PaddingBottom + strokeThickness * 2; + + // Respect explicit size requests + var requestedWidth = WidthRequest >= 0 ? (float)WidthRequest : availableSize.Width; + var requestedHeight = HeightRequest >= 0 ? (float)HeightRequest : availableSize.Height; + + var childAvailable = new SKSize( + Math.Max(0, requestedWidth - paddingWidth), + Math.Max(0, requestedHeight - paddingHeight)); + + var maxChildSize = SKSize.Empty; + + foreach (var child in Children) + { + var childSize = child.Measure(childAvailable); + maxChildSize = new SKSize( + Math.Max(maxChildSize.Width, childSize.Width), + Math.Max(maxChildSize.Height, childSize.Height)); + } + + // Use requested size if set, otherwise use child size + padding + var width = WidthRequest >= 0 ? (float)WidthRequest : maxChildSize.Width + paddingWidth; + var height = HeightRequest >= 0 ? (float)HeightRequest : maxChildSize.Height + paddingHeight; + + return new SKSize(width, height); + } + + protected override SKRect ArrangeOverride(SKRect bounds) + { + var contentBounds = GetContentBounds(bounds); + + foreach (var child in Children) + { + // Apply child's margin + var margin = child.Margin; + var marginedBounds = new SKRect( + contentBounds.Left + (float)margin.Left, + contentBounds.Top + (float)margin.Top, + contentBounds.Right - (float)margin.Right, + contentBounds.Bottom - (float)margin.Bottom); + child.Arrange(marginedBounds); + } + + return bounds; + } +} + +/// +/// Frame control - a Border with shadow enabled by default. +/// Mimics the MAUI Frame control appearance. +/// +public class SkiaFrame : SkiaBorder +{ + public SkiaFrame() + { + HasShadow = true; + CornerRadius = 4; + SetPadding(10); + BackgroundColor = SKColors.White; + Stroke = SKColors.Transparent; + StrokeThickness = 0; + } } diff --git a/Views/SkiaBoxView.cs b/Views/SkiaBoxView.cs index 7eafb60..3a8f100 100644 --- a/Views/SkiaBoxView.cs +++ b/Views/SkiaBoxView.cs @@ -1,93 +1,66 @@ -using System; -using Microsoft.Maui.Controls; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using SkiaSharp; namespace Microsoft.Maui.Platform; +/// +/// Skia-rendered BoxView - a simple colored rectangle. +/// public class SkiaBoxView : SkiaView { - public static readonly BindableProperty ColorProperty = BindableProperty.Create("Color", typeof(SKColor), typeof(SkiaBoxView), (object)SKColors.Transparent, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaBoxView)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + public static readonly BindableProperty ColorProperty = + BindableProperty.Create(nameof(Color), typeof(SKColor), typeof(SkiaBoxView), SKColors.Transparent, + propertyChanged: (b, o, n) => ((SkiaBoxView)b).Invalidate()); - public static readonly BindableProperty CornerRadiusProperty = BindableProperty.Create("CornerRadius", typeof(float), typeof(SkiaBoxView), (object)0f, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaBoxView)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + public static readonly BindableProperty CornerRadiusProperty = + BindableProperty.Create(nameof(CornerRadius), typeof(float), typeof(SkiaBoxView), 0f, + propertyChanged: (b, o, n) => ((SkiaBoxView)b).Invalidate()); - public SKColor Color - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKColor)((BindableObject)this).GetValue(ColorProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(ColorProperty, (object)value); - } - } + public SKColor Color + { + get => (SKColor)GetValue(ColorProperty); + set => SetValue(ColorProperty, value); + } - public float CornerRadius - { - get - { - return (float)((BindableObject)this).GetValue(CornerRadiusProperty); - } - set - { - ((BindableObject)this).SetValue(CornerRadiusProperty, (object)value); - } - } + public float CornerRadius + { + get => (float)GetValue(CornerRadiusProperty); + set => SetValue(CornerRadiusProperty, value); + } - protected override void OnDraw(SKCanvas canvas, SKRect bounds) - { - //IL_0000: Unknown result type (might be due to invalid IL or missing references) - //IL_0005: Unknown result type (might be due to invalid IL or missing references) - //IL_0007: Unknown result type (might be due to invalid IL or missing references) - //IL_0011: Unknown result type (might be due to invalid IL or missing references) - //IL_0018: Unknown result type (might be due to invalid IL or missing references) - //IL_0020: Expected O, but got Unknown - //IL_0044: Unknown result type (might be due to invalid IL or missing references) - //IL_002e: Unknown result type (might be due to invalid IL or missing references) - SKPaint val = new SKPaint - { - Color = Color, - Style = (SKPaintStyle)0, - IsAntialias = true - }; - try - { - if (CornerRadius > 0f) - { - canvas.DrawRoundRect(bounds, CornerRadius, CornerRadius, val); - } - else - { - canvas.DrawRect(bounds, val); - } - } - finally - { - ((IDisposable)val)?.Dispose(); - } - } + protected override void OnDraw(SKCanvas canvas, SKRect bounds) + { + using var paint = new SKPaint + { + Color = Color, + Style = SKPaintStyle.Fill, + IsAntialias = true + }; - protected override SKSize MeasureOverride(SKSize availableSize) - { - //IL_008c: Unknown result type (might be due to invalid IL or missing references) - float num = ((base.WidthRequest >= 0.0) ? ((float)base.WidthRequest) : (float.IsInfinity(((SKSize)(ref availableSize)).Width) ? 40f : ((SKSize)(ref availableSize)).Width)); - float num2 = ((base.HeightRequest >= 0.0) ? ((float)base.HeightRequest) : (float.IsInfinity(((SKSize)(ref availableSize)).Height) ? 40f : ((SKSize)(ref availableSize)).Height)); - if (float.IsNaN(num)) - { - num = 40f; - } - if (float.IsNaN(num2)) - { - num2 = 40f; - } - return new SKSize(num, num2); - } + if (CornerRadius > 0) + { + canvas.DrawRoundRect(bounds, CornerRadius, CornerRadius, paint); + } + else + { + canvas.DrawRect(bounds, paint); + } + } + + protected override SKSize MeasureOverride(SKSize availableSize) + { + // BoxView uses explicit size or a default size when in unbounded context + var width = WidthRequest >= 0 ? (float)WidthRequest : + (float.IsInfinity(availableSize.Width) ? 40f : availableSize.Width); + var height = HeightRequest >= 0 ? (float)HeightRequest : + (float.IsInfinity(availableSize.Height) ? 40f : availableSize.Height); + + // Ensure no NaN values + if (float.IsNaN(width)) width = 40f; + if (float.IsNaN(height)) height = 40f; + + return new SKSize(width, height); + } } diff --git a/Views/SkiaButton.cs b/Views/SkiaButton.cs index 066072e..5e584f2 100644 --- a/Views/SkiaButton.cs +++ b/Views/SkiaButton.cs @@ -1,951 +1,713 @@ -using System; -using System.Windows.Input; -using Microsoft.Maui.Controls; -using Microsoft.Maui.Platform.Linux.Rendering; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using SkiaSharp; +using Microsoft.Maui.Platform.Linux.Rendering; namespace Microsoft.Maui.Platform; +/// +/// Skia-rendered button control with full XAML styling support. +/// public class SkiaButton : SkiaView { - public static readonly BindableProperty TextProperty = BindableProperty.Create("Text", typeof(string), typeof(SkiaButton), (object)"", (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaButton)(object)b).OnTextChanged(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + #region BindableProperties - public static readonly BindableProperty TextColorProperty = BindableProperty.Create("TextColor", typeof(SKColor), typeof(SkiaButton), (object)SKColors.White, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaButton)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for Text. + /// + public static readonly BindableProperty TextProperty = + BindableProperty.Create( + nameof(Text), + typeof(string), + typeof(SkiaButton), + "", + propertyChanged: (b, o, n) => ((SkiaButton)b).OnTextChanged()); - public static readonly BindableProperty ButtonBackgroundColorProperty = BindableProperty.Create("ButtonBackgroundColor", typeof(SKColor), typeof(SkiaButton), (object)new SKColor((byte)33, (byte)150, (byte)243), (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaButton)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for TextColor. + /// + public static readonly BindableProperty TextColorProperty = + BindableProperty.Create( + nameof(TextColor), + typeof(SKColor), + typeof(SkiaButton), + SKColors.White, + propertyChanged: (b, o, n) => ((SkiaButton)b).Invalidate()); - public static readonly BindableProperty PressedBackgroundColorProperty = BindableProperty.Create("PressedBackgroundColor", typeof(SKColor), typeof(SkiaButton), (object)new SKColor((byte)25, (byte)118, (byte)210), (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaButton)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for ButtonBackgroundColor (distinct from base BackgroundColor). + /// + public static readonly BindableProperty ButtonBackgroundColorProperty = + BindableProperty.Create( + nameof(ButtonBackgroundColor), + typeof(SKColor), + typeof(SkiaButton), + new SKColor(0x21, 0x96, 0xF3), // Material Blue + propertyChanged: (b, o, n) => ((SkiaButton)b).Invalidate()); - public static readonly BindableProperty DisabledBackgroundColorProperty = BindableProperty.Create("DisabledBackgroundColor", typeof(SKColor), typeof(SkiaButton), (object)new SKColor((byte)189, (byte)189, (byte)189), (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaButton)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for PressedBackgroundColor. + /// + public static readonly BindableProperty PressedBackgroundColorProperty = + BindableProperty.Create( + nameof(PressedBackgroundColor), + typeof(SKColor), + typeof(SkiaButton), + new SKColor(0x19, 0x76, 0xD2), + propertyChanged: (b, o, n) => ((SkiaButton)b).Invalidate()); - public static readonly BindableProperty HoveredBackgroundColorProperty = BindableProperty.Create("HoveredBackgroundColor", typeof(SKColor), typeof(SkiaButton), (object)new SKColor((byte)66, (byte)165, (byte)245), (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaButton)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for DisabledBackgroundColor. + /// + public static readonly BindableProperty DisabledBackgroundColorProperty = + BindableProperty.Create( + nameof(DisabledBackgroundColor), + typeof(SKColor), + typeof(SkiaButton), + new SKColor(0xBD, 0xBD, 0xBD), + propertyChanged: (b, o, n) => ((SkiaButton)b).Invalidate()); - public static readonly BindableProperty BorderColorProperty = BindableProperty.Create("BorderColor", typeof(SKColor), typeof(SkiaButton), (object)SKColors.Transparent, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaButton)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for HoveredBackgroundColor. + /// + public static readonly BindableProperty HoveredBackgroundColorProperty = + BindableProperty.Create( + nameof(HoveredBackgroundColor), + typeof(SKColor), + typeof(SkiaButton), + new SKColor(0x42, 0xA5, 0xF5), + propertyChanged: (b, o, n) => ((SkiaButton)b).Invalidate()); - public static readonly BindableProperty FontFamilyProperty = BindableProperty.Create("FontFamily", typeof(string), typeof(SkiaButton), (object)"Sans", (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaButton)(object)b).OnFontChanged(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for BorderColor. + /// + public static readonly BindableProperty BorderColorProperty = + BindableProperty.Create( + nameof(BorderColor), + typeof(SKColor), + typeof(SkiaButton), + SKColors.Transparent, + propertyChanged: (b, o, n) => ((SkiaButton)b).Invalidate()); - public static readonly BindableProperty FontSizeProperty = BindableProperty.Create("FontSize", typeof(float), typeof(SkiaButton), (object)14f, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaButton)(object)b).OnFontChanged(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for FontFamily. + /// + public static readonly BindableProperty FontFamilyProperty = + BindableProperty.Create( + nameof(FontFamily), + typeof(string), + typeof(SkiaButton), + "Sans", + propertyChanged: (b, o, n) => ((SkiaButton)b).OnFontChanged()); - public static readonly BindableProperty IsBoldProperty = BindableProperty.Create("IsBold", typeof(bool), typeof(SkiaButton), (object)false, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaButton)(object)b).OnFontChanged(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for FontSize. + /// + public static readonly BindableProperty FontSizeProperty = + BindableProperty.Create( + nameof(FontSize), + typeof(float), + typeof(SkiaButton), + 14f, + propertyChanged: (b, o, n) => ((SkiaButton)b).OnFontChanged()); - public static readonly BindableProperty IsItalicProperty = BindableProperty.Create("IsItalic", typeof(bool), typeof(SkiaButton), (object)false, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaButton)(object)b).OnFontChanged(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for IsBold. + /// + public static readonly BindableProperty IsBoldProperty = + BindableProperty.Create( + nameof(IsBold), + typeof(bool), + typeof(SkiaButton), + false, + propertyChanged: (b, o, n) => ((SkiaButton)b).OnFontChanged()); - public static readonly BindableProperty CharacterSpacingProperty = BindableProperty.Create("CharacterSpacing", typeof(float), typeof(SkiaButton), (object)0f, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaButton)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for IsItalic. + /// + public static readonly BindableProperty IsItalicProperty = + BindableProperty.Create( + nameof(IsItalic), + typeof(bool), + typeof(SkiaButton), + false, + propertyChanged: (b, o, n) => ((SkiaButton)b).OnFontChanged()); - public static readonly BindableProperty CornerRadiusProperty = BindableProperty.Create("CornerRadius", typeof(float), typeof(SkiaButton), (object)4f, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaButton)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for CharacterSpacing. + /// + public static readonly BindableProperty CharacterSpacingProperty = + BindableProperty.Create( + nameof(CharacterSpacing), + typeof(float), + typeof(SkiaButton), + 0f, + propertyChanged: (b, o, n) => ((SkiaButton)b).Invalidate()); - public static readonly BindableProperty BorderWidthProperty = BindableProperty.Create("BorderWidth", typeof(float), typeof(SkiaButton), (object)0f, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaButton)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for CornerRadius. + /// + public static readonly BindableProperty CornerRadiusProperty = + BindableProperty.Create( + nameof(CornerRadius), + typeof(float), + typeof(SkiaButton), + 4f, + propertyChanged: (b, o, n) => ((SkiaButton)b).Invalidate()); - public static readonly BindableProperty PaddingProperty = BindableProperty.Create("Padding", typeof(SKRect), typeof(SkiaButton), (object)new SKRect(16f, 8f, 16f, 8f), (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaButton)(object)b).InvalidateMeasure(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for BorderWidth. + /// + public static readonly BindableProperty BorderWidthProperty = + BindableProperty.Create( + nameof(BorderWidth), + typeof(float), + typeof(SkiaButton), + 0f, + propertyChanged: (b, o, n) => ((SkiaButton)b).Invalidate()); - public static readonly BindableProperty CommandProperty = BindableProperty.Create("Command", typeof(ICommand), typeof(SkiaButton), (object)null, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaButton)(object)b).OnCommandChanged((ICommand)o, (ICommand)n); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for Padding. + /// + public static readonly BindableProperty PaddingProperty = + BindableProperty.Create( + nameof(Padding), + typeof(SKRect), + typeof(SkiaButton), + new SKRect(16, 8, 16, 8), + propertyChanged: (b, o, n) => ((SkiaButton)b).InvalidateMeasure()); - public static readonly BindableProperty CommandParameterProperty = BindableProperty.Create("CommandParameter", typeof(object), typeof(SkiaButton), (object)null, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)null, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for Command. + /// + public static readonly BindableProperty CommandProperty = + BindableProperty.Create( + nameof(Command), + typeof(System.Windows.Input.ICommand), + typeof(SkiaButton), + null, + propertyChanged: (b, o, n) => ((SkiaButton)b).OnCommandChanged((System.Windows.Input.ICommand?)o, (System.Windows.Input.ICommand?)n)); - public static readonly BindableProperty ImageSourceProperty = BindableProperty.Create("ImageSource", typeof(SKBitmap), typeof(SkiaButton), (object)null, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaButton)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for CommandParameter. + /// + public static readonly BindableProperty CommandParameterProperty = + BindableProperty.Create( + nameof(CommandParameter), + typeof(object), + typeof(SkiaButton), + null); - public static readonly BindableProperty ImageSpacingProperty = BindableProperty.Create("ImageSpacing", typeof(float), typeof(SkiaButton), (object)8f, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaButton)(object)b).InvalidateMeasure(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + #endregion - public static readonly BindableProperty ContentLayoutPositionProperty = BindableProperty.Create("ContentLayoutPosition", typeof(int), typeof(SkiaButton), (object)0, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaButton)(object)b).InvalidateMeasure(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + #region Properties - private bool _focusFromKeyboard; + /// + /// Gets or sets the button text. + /// + public string Text + { + get => (string)GetValue(TextProperty); + set => SetValue(TextProperty, value); + } - public string Text - { - get - { - return (string)((BindableObject)this).GetValue(TextProperty); - } - set - { - ((BindableObject)this).SetValue(TextProperty, (object)value); - } - } + /// + /// Gets or sets the text color. + /// + public SKColor TextColor + { + get => (SKColor)GetValue(TextColorProperty); + set => SetValue(TextColorProperty, value); + } - public SKColor TextColor - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKColor)((BindableObject)this).GetValue(TextColorProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(TextColorProperty, (object)value); - } - } + /// + /// Gets or sets the button background color. + /// + public SKColor ButtonBackgroundColor + { + get => (SKColor)GetValue(ButtonBackgroundColorProperty); + set => SetValue(ButtonBackgroundColorProperty, value); + } - public SKColor ButtonBackgroundColor - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKColor)((BindableObject)this).GetValue(ButtonBackgroundColorProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(ButtonBackgroundColorProperty, (object)value); - } - } + /// + /// Gets or sets the pressed background color. + /// + public SKColor PressedBackgroundColor + { + get => (SKColor)GetValue(PressedBackgroundColorProperty); + set => SetValue(PressedBackgroundColorProperty, value); + } - public SKColor PressedBackgroundColor - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKColor)((BindableObject)this).GetValue(PressedBackgroundColorProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(PressedBackgroundColorProperty, (object)value); - } - } + /// + /// Gets or sets the disabled background color. + /// + public SKColor DisabledBackgroundColor + { + get => (SKColor)GetValue(DisabledBackgroundColorProperty); + set => SetValue(DisabledBackgroundColorProperty, value); + } - public SKColor DisabledBackgroundColor - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKColor)((BindableObject)this).GetValue(DisabledBackgroundColorProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(DisabledBackgroundColorProperty, (object)value); - } - } + /// + /// Gets or sets the hovered background color. + /// + public SKColor HoveredBackgroundColor + { + get => (SKColor)GetValue(HoveredBackgroundColorProperty); + set => SetValue(HoveredBackgroundColorProperty, value); + } - public SKColor HoveredBackgroundColor - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKColor)((BindableObject)this).GetValue(HoveredBackgroundColorProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(HoveredBackgroundColorProperty, (object)value); - } - } + /// + /// Gets or sets the border color. + /// + public SKColor BorderColor + { + get => (SKColor)GetValue(BorderColorProperty); + set => SetValue(BorderColorProperty, value); + } - public SKColor BorderColor - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKColor)((BindableObject)this).GetValue(BorderColorProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(BorderColorProperty, (object)value); - } - } + /// + /// Gets or sets the font family. + /// + public string FontFamily + { + get => (string)GetValue(FontFamilyProperty); + set => SetValue(FontFamilyProperty, value); + } - public string FontFamily - { - get - { - return (string)((BindableObject)this).GetValue(FontFamilyProperty); - } - set - { - ((BindableObject)this).SetValue(FontFamilyProperty, (object)value); - } - } + /// + /// Gets or sets the font size. + /// + public float FontSize + { + get => (float)GetValue(FontSizeProperty); + set => SetValue(FontSizeProperty, value); + } - public float FontSize - { - get - { - return (float)((BindableObject)this).GetValue(FontSizeProperty); - } - set - { - ((BindableObject)this).SetValue(FontSizeProperty, (object)value); - } - } + /// + /// Gets or sets whether the text is bold. + /// + public bool IsBold + { + get => (bool)GetValue(IsBoldProperty); + set => SetValue(IsBoldProperty, value); + } - public bool IsBold - { - get - { - return (bool)((BindableObject)this).GetValue(IsBoldProperty); - } - set - { - ((BindableObject)this).SetValue(IsBoldProperty, (object)value); - } - } + /// + /// Gets or sets whether the text is italic. + /// + public bool IsItalic + { + get => (bool)GetValue(IsItalicProperty); + set => SetValue(IsItalicProperty, value); + } - public bool IsItalic - { - get - { - return (bool)((BindableObject)this).GetValue(IsItalicProperty); - } - set - { - ((BindableObject)this).SetValue(IsItalicProperty, (object)value); - } - } + /// + /// Gets or sets the character spacing. + /// + public float CharacterSpacing + { + get => (float)GetValue(CharacterSpacingProperty); + set => SetValue(CharacterSpacingProperty, value); + } - public float CharacterSpacing - { - get - { - return (float)((BindableObject)this).GetValue(CharacterSpacingProperty); - } - set - { - ((BindableObject)this).SetValue(CharacterSpacingProperty, (object)value); - } - } + /// + /// Gets or sets the corner radius. + /// + public float CornerRadius + { + get => (float)GetValue(CornerRadiusProperty); + set => SetValue(CornerRadiusProperty, value); + } - public float CornerRadius - { - get - { - return (float)((BindableObject)this).GetValue(CornerRadiusProperty); - } - set - { - ((BindableObject)this).SetValue(CornerRadiusProperty, (object)value); - } - } + /// + /// Gets or sets the border width. + /// + public float BorderWidth + { + get => (float)GetValue(BorderWidthProperty); + set => SetValue(BorderWidthProperty, value); + } - public float BorderWidth - { - get - { - return (float)((BindableObject)this).GetValue(BorderWidthProperty); - } - set - { - ((BindableObject)this).SetValue(BorderWidthProperty, (object)value); - } - } + /// + /// Gets or sets the padding. + /// + public SKRect Padding + { + get => (SKRect)GetValue(PaddingProperty); + set => SetValue(PaddingProperty, value); + } - public SKRect Padding - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKRect)((BindableObject)this).GetValue(PaddingProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(PaddingProperty, (object)value); - } - } + /// + /// Gets or sets the command to execute when clicked. + /// + public System.Windows.Input.ICommand? Command + { + get => (System.Windows.Input.ICommand?)GetValue(CommandProperty); + set => SetValue(CommandProperty, value); + } - public ICommand? Command - { - get - { - return (ICommand)((BindableObject)this).GetValue(CommandProperty); - } - set - { - ((BindableObject)this).SetValue(CommandProperty, (object)value); - } - } + /// + /// Gets or sets the command parameter. + /// + public object? CommandParameter + { + get => GetValue(CommandParameterProperty); + set => SetValue(CommandParameterProperty, value); + } - public object? CommandParameter - { - get - { - return ((BindableObject)this).GetValue(CommandParameterProperty); - } - set - { - ((BindableObject)this).SetValue(CommandParameterProperty, value); - } - } + /// + /// Gets whether the button is currently pressed. + /// + public bool IsPressed { get; private set; } - public SKBitmap? ImageSource - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - //IL_0011: Expected O, but got Unknown - return (SKBitmap)((BindableObject)this).GetValue(ImageSourceProperty); - } - set - { - ((BindableObject)this).SetValue(ImageSourceProperty, (object)value); - } - } + /// + /// Gets whether the pointer is currently over the button. + /// + public bool IsHovered { get; private set; } - public float ImageSpacing - { - get - { - return (float)((BindableObject)this).GetValue(ImageSpacingProperty); - } - set - { - ((BindableObject)this).SetValue(ImageSpacingProperty, (object)value); - } - } + #endregion - public int ContentLayoutPosition - { - get - { - return (int)((BindableObject)this).GetValue(ContentLayoutPositionProperty); - } - set - { - ((BindableObject)this).SetValue(ContentLayoutPositionProperty, (object)value); - } - } + private bool _focusFromKeyboard; - public bool IsPressed { get; private set; } + /// + /// Event raised when the button is clicked. + /// + public event EventHandler? Clicked; - public bool IsHovered { get; private set; } + /// + /// Event raised when the button is pressed. + /// + public event EventHandler? Pressed; - public event EventHandler? Clicked; + /// + /// Event raised when the button is released. + /// + public event EventHandler? Released; - public event EventHandler? Pressed; + public SkiaButton() + { + IsFocusable = true; + } - public event EventHandler? Released; + private void OnTextChanged() + { + InvalidateMeasure(); + Invalidate(); + } - public SkiaButton() - { - base.IsFocusable = true; - } + private void OnFontChanged() + { + InvalidateMeasure(); + Invalidate(); + } - private void OnTextChanged() - { - InvalidateMeasure(); - Invalidate(); - } + private void OnCommandChanged(System.Windows.Input.ICommand? oldCommand, System.Windows.Input.ICommand? newCommand) + { + if (oldCommand != null) + { + oldCommand.CanExecuteChanged -= OnCanExecuteChanged; + } - private void OnFontChanged() - { - InvalidateMeasure(); - Invalidate(); - } + if (newCommand != null) + { + newCommand.CanExecuteChanged += OnCanExecuteChanged; + UpdateIsEnabled(); + } + } - private void OnCommandChanged(ICommand? oldCommand, ICommand? newCommand) - { - if (oldCommand != null) - { - oldCommand.CanExecuteChanged -= OnCanExecuteChanged; - } - if (newCommand != null) - { - newCommand.CanExecuteChanged += OnCanExecuteChanged; - UpdateIsEnabled(); - } - } + private void OnCanExecuteChanged(object? sender, EventArgs e) + { + UpdateIsEnabled(); + } - private void OnCanExecuteChanged(object? sender, EventArgs e) - { - UpdateIsEnabled(); - } + private void UpdateIsEnabled() + { + if (Command != null) + { + IsEnabled = Command.CanExecute(CommandParameter); + } + } - private void UpdateIsEnabled() - { - if (Command != null) - { - base.IsEnabled = Command.CanExecute(CommandParameter); - } - } + protected override void OnDraw(SKCanvas canvas, SKRect bounds) + { + // Check if this is a "text only" button (transparent background) + var isTextOnly = ButtonBackgroundColor.Alpha == 0; - protected override void OnDraw(SKCanvas canvas, SKRect bounds) - { - //IL_0001: Unknown result type (might be due to invalid IL or missing references) - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - //IL_0025: Unknown result type (might be due to invalid IL or missing references) - //IL_001e: Unknown result type (might be due to invalid IL or missing references) - //IL_006e: Unknown result type (might be due to invalid IL or missing references) - //IL_0073: Unknown result type (might be due to invalid IL or missing references) - //IL_0045: Unknown result type (might be due to invalid IL or missing references) - //IL_0039: Unknown result type (might be due to invalid IL or missing references) - //IL_002a: Unknown result type (might be due to invalid IL or missing references) - //IL_0065: Unknown result type (might be due to invalid IL or missing references) - //IL_0059: Unknown result type (might be due to invalid IL or missing references) - //IL_004a: Unknown result type (might be due to invalid IL or missing references) - //IL_008f: Unknown result type (might be due to invalid IL or missing references) - //IL_0096: Unknown result type (might be due to invalid IL or missing references) - //IL_009c: Expected O, but got Unknown - //IL_006a: Unknown result type (might be due to invalid IL or missing references) - //IL_00a6: Unknown result type (might be due to invalid IL or missing references) - //IL_00ab: Unknown result type (might be due to invalid IL or missing references) - //IL_00ac: Unknown result type (might be due to invalid IL or missing references) - //IL_00b2: Unknown result type (might be due to invalid IL or missing references) - //IL_00b9: Unknown result type (might be due to invalid IL or missing references) - //IL_00c2: Expected O, but got Unknown - //IL_00e7: Unknown result type (might be due to invalid IL or missing references) - //IL_00ec: Unknown result type (might be due to invalid IL or missing references) - //IL_0089: Unknown result type (might be due to invalid IL or missing references) - //IL_01ca: Unknown result type (might be due to invalid IL or missing references) - //IL_00f8: Unknown result type (might be due to invalid IL or missing references) - //IL_00fd: Unknown result type (might be due to invalid IL or missing references) - //IL_00ff: Unknown result type (might be due to invalid IL or missing references) - //IL_0109: Unknown result type (might be due to invalid IL or missing references) - //IL_0110: Unknown result type (might be due to invalid IL or missing references) - //IL_0117: Unknown result type (might be due to invalid IL or missing references) - //IL_0125: Expected O, but got Unknown - //IL_014f: Unknown result type (might be due to invalid IL or missing references) - //IL_0154: Unknown result type (might be due to invalid IL or missing references) - //IL_0166: Unknown result type (might be due to invalid IL or missing references) - //IL_0170: Unknown result type (might be due to invalid IL or missing references) - //IL_0177: Unknown result type (might be due to invalid IL or missing references) - //IL_017e: Unknown result type (might be due to invalid IL or missing references) - //IL_018b: Expected O, but got Unknown - //IL_018b: Unknown result type (might be due to invalid IL or missing references) - //IL_0198: Unknown result type (might be due to invalid IL or missing references) - //IL_019f: Expected O, but got Unknown - SKColor buttonBackgroundColor = ButtonBackgroundColor; - bool flag = ((SKColor)(ref buttonBackgroundColor)).Alpha == 0; - SKColor color = (SKColor)((!base.IsEnabled) ? (flag ? SKColors.Transparent : DisabledBackgroundColor) : (IsPressed ? (flag ? new SKColor((byte)0, (byte)0, (byte)0, (byte)20) : PressedBackgroundColor) : ((!IsHovered) ? ButtonBackgroundColor : (flag ? new SKColor((byte)0, (byte)0, (byte)0, (byte)10) : HoveredBackgroundColor)))); - if (base.IsEnabled && !IsPressed && !flag) - { - DrawShadow(canvas, bounds); - } - SKRoundRect val = new SKRoundRect(bounds, CornerRadius); - if (((SKColor)(ref color)).Alpha > 0) - { - SKPaint val2 = new SKPaint - { - Color = color, - IsAntialias = true, - Style = (SKPaintStyle)0 - }; - try - { - canvas.DrawRoundRect(val, val2); - } - finally - { - ((IDisposable)val2)?.Dispose(); - } - } - if (BorderWidth > 0f && BorderColor != SKColors.Transparent) - { - SKPaint val3 = new SKPaint - { - Color = BorderColor, - IsAntialias = true, - Style = (SKPaintStyle)1, - StrokeWidth = BorderWidth - }; - try - { - canvas.DrawRoundRect(val, val3); - } - finally - { - ((IDisposable)val3)?.Dispose(); - } - } - if (base.IsFocused && _focusFromKeyboard) - { - SKPaint val4 = new SKPaint - { - Color = new SKColor((byte)33, (byte)150, (byte)243, (byte)128), - IsAntialias = true, - Style = (SKPaintStyle)1, - StrokeWidth = 2f - }; - try - { - SKRoundRect val5 = new SKRoundRect(bounds, CornerRadius + 2f); - val5.Inflate(2f, 2f); - canvas.DrawRoundRect(val5, val4); - } - finally - { - ((IDisposable)val4)?.Dispose(); - } - } - DrawContent(canvas, bounds, flag); - } + // Determine background color based on state + SKColor bgColor; + if (!IsEnabled) + { + bgColor = isTextOnly ? SKColors.Transparent : DisabledBackgroundColor; + } + else if (IsPressed) + { + // For text-only buttons, use a subtle press effect + bgColor = isTextOnly ? new SKColor(0, 0, 0, 20) : PressedBackgroundColor; + } + else if (IsHovered) + { + // For text-only buttons, use a subtle hover effect instead of full background + bgColor = isTextOnly ? new SKColor(0, 0, 0, 10) : HoveredBackgroundColor; + } + else + { + bgColor = ButtonBackgroundColor; + } - private void DrawContent(SKCanvas canvas, SKRect bounds, bool isTextOnly) - { - //IL_0021: Unknown result type (might be due to invalid IL or missing references) - //IL_0027: Expected O, but got Unknown - //IL_005d: Unknown result type (might be due to invalid IL or missing references) - //IL_0063: Expected O, but got Unknown - //IL_006c: Unknown result type (might be due to invalid IL or missing references) - //IL_0071: Unknown result type (might be due to invalid IL or missing references) - //IL_007a: Unknown result type (might be due to invalid IL or missing references) - //IL_007f: Unknown result type (might be due to invalid IL or missing references) - //IL_00f8: Unknown result type (might be due to invalid IL or missing references) - //IL_00fd: Unknown result type (might be due to invalid IL or missing references) - //IL_00ff: Unknown result type (might be due to invalid IL or missing references) - //IL_0104: Unknown result type (might be due to invalid IL or missing references) - //IL_0105: Unknown result type (might be due to invalid IL or missing references) - //IL_010b: Unknown result type (might be due to invalid IL or missing references) - //IL_0113: Expected O, but got Unknown - //IL_0097: Unknown result type (might be due to invalid IL or missing references) - //IL_009c: Unknown result type (might be due to invalid IL or missing references) - //IL_00b0: Unknown result type (might be due to invalid IL or missing references) - //IL_00b5: Unknown result type (might be due to invalid IL or missing references) - //IL_00c9: Unknown result type (might be due to invalid IL or missing references) - //IL_00ce: Unknown result type (might be due to invalid IL or missing references) - //IL_00e1: Unknown result type (might be due to invalid IL or missing references) - //IL_00e6: Unknown result type (might be due to invalid IL or missing references) - //IL_00ef: Unknown result type (might be due to invalid IL or missing references) - //IL_00f4: Unknown result type (might be due to invalid IL or missing references) - //IL_0115: Unknown result type (might be due to invalid IL or missing references) - //IL_03b9: Unknown result type (might be due to invalid IL or missing references) - //IL_03be: Unknown result type (might be due to invalid IL or missing references) - //IL_03c7: Expected O, but got Unknown - //IL_03fc: Unknown result type (might be due to invalid IL or missing references) - //IL_03e5: Unknown result type (might be due to invalid IL or missing references) - SKFontStyle style = new SKFontStyle((SKFontStyleWeight)(IsBold ? 700 : 400), (SKFontStyleWidth)5, (SKFontStyleSlant)(IsItalic ? 1 : 0)); - SKFont val = new SKFont(SkiaRenderingEngine.Current?.ResourceCache.GetTypeface(FontFamily, style) ?? SKTypeface.Default, FontSize, 1f, 0f); - try - { - SKColor textColor; - SKColor color; - if (!base.IsEnabled) - { - textColor = TextColor; - color = ((SKColor)(ref textColor)).WithAlpha((byte)128); - } - else if (isTextOnly && (IsHovered || IsPressed)) - { - textColor = TextColor; - byte num = (byte)Math.Max(0, ((SKColor)(ref textColor)).Red - 40); - textColor = TextColor; - byte num2 = (byte)Math.Max(0, ((SKColor)(ref textColor)).Green - 40); - textColor = TextColor; - byte num3 = (byte)Math.Max(0, ((SKColor)(ref textColor)).Blue - 40); - textColor = TextColor; - color = new SKColor(num, num2, num3, ((SKColor)(ref textColor)).Alpha); - } - else - { - color = TextColor; - } - SKPaint val2 = new SKPaint(val) - { - Color = color, - IsAntialias = true - }; - try - { - SKRect val3 = default(SKRect); - bool flag = !string.IsNullOrEmpty(Text); - if (flag) - { - val2.MeasureText(Text, ref val3); - } - bool flag2 = ImageSource != null; - float num4 = 0f; - float num5 = 0f; - if (flag2) - { - float num6 = Math.Min(((SKRect)(ref bounds)).Height - 8f, 24f); - float num7 = Math.Min(num6 / (float)ImageSource.Width, num6 / (float)ImageSource.Height); - num4 = (float)ImageSource.Width * num7; - num5 = (float)ImageSource.Height * num7; - } - bool flag3 = ContentLayoutPosition == 0 || ContentLayoutPosition == 2; - float num8; - float num9; - if (flag2 && flag) - { - if (flag3) - { - num8 = num4 + ImageSpacing + ((SKRect)(ref val3)).Width; - num9 = Math.Max(num5, ((SKRect)(ref val3)).Height); - } - else - { - num8 = Math.Max(num4, ((SKRect)(ref val3)).Width); - num9 = num5 + ImageSpacing + ((SKRect)(ref val3)).Height; - } - } - else if (flag2) - { - num8 = num4; - num9 = num5; - } - else - { - num8 = ((SKRect)(ref val3)).Width; - num9 = ((SKRect)(ref val3)).Height; - } - float num10 = ((SKRect)(ref bounds)).MidX - num8 / 2f; - float num11 = ((SKRect)(ref bounds)).MidY - num9 / 2f; - if (flag2) - { - float num12 = 0f; - float num13 = 0f; - float num14; - float num15; - switch (ContentLayoutPosition) - { - case 1: - num14 = ((SKRect)(ref bounds)).MidX - num4 / 2f; - num15 = num11; - num12 = ((SKRect)(ref bounds)).MidX - ((SKRect)(ref val3)).Width / 2f; - num13 = num11 + num5 + ImageSpacing - ((SKRect)(ref val3)).Top; - break; - case 2: - num12 = num10; - num13 = ((SKRect)(ref bounds)).MidY - ((SKRect)(ref val3)).MidY; - num14 = num10 + ((SKRect)(ref val3)).Width + ImageSpacing; - num15 = ((SKRect)(ref bounds)).MidY - num5 / 2f; - break; - case 3: - num12 = ((SKRect)(ref bounds)).MidX - ((SKRect)(ref val3)).Width / 2f; - num13 = num11 - ((SKRect)(ref val3)).Top; - num14 = ((SKRect)(ref bounds)).MidX - num4 / 2f; - num15 = num11 + ((SKRect)(ref val3)).Height + ImageSpacing; - break; - default: - num14 = num10; - num15 = ((SKRect)(ref bounds)).MidY - num5 / 2f; - num12 = num10 + num4 + ImageSpacing; - num13 = ((SKRect)(ref bounds)).MidY - ((SKRect)(ref val3)).MidY; - break; - } - SKRect val4 = default(SKRect); - ((SKRect)(ref val4))._002Ector(num14, num15, num14 + num4, num15 + num5); - SKPaint val5 = new SKPaint - { - IsAntialias = true - }; - try - { - if (!base.IsEnabled) - { - val5.ColorFilter = SKColorFilter.CreateBlendMode(new SKColor((byte)128, (byte)128, (byte)128, (byte)128), (SKBlendMode)5); - } - canvas.DrawBitmap(ImageSource, val4, val5); - if (flag) - { - canvas.DrawText(Text, num12, num13, val2); - } - return; - } - finally - { - ((IDisposable)val5)?.Dispose(); - } - } - if (flag) - { - float num16 = ((SKRect)(ref bounds)).MidX - ((SKRect)(ref val3)).MidX; - float num17 = ((SKRect)(ref bounds)).MidY - ((SKRect)(ref val3)).MidY; - canvas.DrawText(Text, num16, num17, val2); - } - } - finally - { - ((IDisposable)val2)?.Dispose(); - } - } - finally - { - ((IDisposable)val)?.Dispose(); - } - } + // Draw shadow (for elevation effect) - skip for text-only buttons + if (IsEnabled && !IsPressed && !isTextOnly) + { + DrawShadow(canvas, bounds); + } - private void DrawShadow(SKCanvas canvas, SKRect bounds) - { - //IL_0000: Unknown result type (might be due to invalid IL or missing references) - //IL_0005: Unknown result type (might be due to invalid IL or missing references) - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - //IL_0015: Unknown result type (might be due to invalid IL or missing references) - //IL_001c: Unknown result type (might be due to invalid IL or missing references) - //IL_002e: Expected O, but got Unknown - //IL_0062: Unknown result type (might be due to invalid IL or missing references) - //IL_006d: Unknown result type (might be due to invalid IL or missing references) - //IL_0073: Expected O, but got Unknown - SKPaint val = new SKPaint - { - Color = new SKColor((byte)0, (byte)0, (byte)0, (byte)50), - IsAntialias = true, - MaskFilter = SKMaskFilter.CreateBlur((SKBlurStyle)0, 4f) - }; - try - { - SKRoundRect val2 = new SKRoundRect(new SKRect(((SKRect)(ref bounds)).Left + 2f, ((SKRect)(ref bounds)).Top + 4f, ((SKRect)(ref bounds)).Right + 2f, ((SKRect)(ref bounds)).Bottom + 4f), CornerRadius); - canvas.DrawRoundRect(val2, val); - } - finally - { - ((IDisposable)val)?.Dispose(); - } - } + // Create rounded rect for background and border + var rect = new SKRoundRect(bounds, CornerRadius); - public override void OnPointerEntered(PointerEventArgs e) - { - if (base.IsEnabled) - { - IsHovered = true; - SkiaVisualStateManager.GoToState(this, "PointerOver"); - Invalidate(); - } - } + // Draw background with rounded corners (skip if fully transparent) + if (bgColor.Alpha > 0) + { + using var bgPaint = new SKPaint + { + Color = bgColor, + IsAntialias = true, + Style = SKPaintStyle.Fill + }; + canvas.DrawRoundRect(rect, bgPaint); + } - public override void OnPointerExited(PointerEventArgs e) - { - IsHovered = false; - if (IsPressed) - { - IsPressed = false; - } - SkiaVisualStateManager.GoToState(this, base.IsEnabled ? "Normal" : "Disabled"); - Invalidate(); - } + // Draw border + if (BorderWidth > 0 && BorderColor != SKColors.Transparent) + { + using var borderPaint = new SKPaint + { + Color = BorderColor, + IsAntialias = true, + Style = SKPaintStyle.Stroke, + StrokeWidth = BorderWidth + }; + canvas.DrawRoundRect(rect, borderPaint); + } - public override void OnPointerPressed(PointerEventArgs e) - { - Console.WriteLine($"[SkiaButton] OnPointerPressed - Text='{Text}', IsEnabled={base.IsEnabled}"); - if (base.IsEnabled) - { - IsPressed = true; - _focusFromKeyboard = false; - SkiaVisualStateManager.GoToState(this, "Pressed"); - Invalidate(); - this.Pressed?.Invoke(this, EventArgs.Empty); - } - } + // Draw focus ring only for keyboard focus + if (IsFocused && _focusFromKeyboard) + { + using var focusPaint = new SKPaint + { + Color = new SKColor(0x21, 0x96, 0xF3, 0x80), + IsAntialias = true, + Style = SKPaintStyle.Stroke, + StrokeWidth = 2 + }; + var focusRect = new SKRoundRect(bounds, CornerRadius + 2); + focusRect.Inflate(2, 2); + canvas.DrawRoundRect(focusRect, focusPaint); + } - public override void OnPointerReleased(PointerEventArgs e) - { - if (base.IsEnabled) - { - bool isPressed = IsPressed; - IsPressed = false; - SkiaVisualStateManager.GoToState(this, IsHovered ? "PointerOver" : "Normal"); - Invalidate(); - this.Released?.Invoke(this, EventArgs.Empty); - if (isPressed) - { - this.Clicked?.Invoke(this, EventArgs.Empty); - Command?.Execute(CommandParameter); - } - } - } + // Draw text + if (!string.IsNullOrEmpty(Text)) + { + var fontStyle = new SKFontStyle( + IsBold ? SKFontStyleWeight.Bold : SKFontStyleWeight.Normal, + SKFontStyleWidth.Normal, + IsItalic ? SKFontStyleSlant.Italic : SKFontStyleSlant.Upright); + var typeface = SkiaRenderingEngine.Current?.ResourceCache.GetTypeface(FontFamily, fontStyle) + ?? SKTypeface.Default; - public override void OnKeyDown(KeyEventArgs e) - { - if (base.IsEnabled && (e.Key == Key.Enter || e.Key == Key.Space)) - { - IsPressed = true; - _focusFromKeyboard = true; - SkiaVisualStateManager.GoToState(this, "Pressed"); - Invalidate(); - this.Pressed?.Invoke(this, EventArgs.Empty); - e.Handled = true; - } - } + using var font = new SKFont(typeface, FontSize); - public override void OnKeyUp(KeyEventArgs e) - { - if (base.IsEnabled && (e.Key == Key.Enter || e.Key == Key.Space)) - { - if (IsPressed) - { - IsPressed = false; - SkiaVisualStateManager.GoToState(this, "Normal"); - Invalidate(); - this.Released?.Invoke(this, EventArgs.Empty); - this.Clicked?.Invoke(this, EventArgs.Empty); - Command?.Execute(CommandParameter); - } - e.Handled = true; - } - } + // For text-only buttons, darken text on hover/press for feedback + SKColor textColorToUse; + if (!IsEnabled) + { + textColorToUse = TextColor.WithAlpha(128); + } + else if (isTextOnly && (IsHovered || IsPressed)) + { + // Darken the text color slightly for hover/press feedback + textColorToUse = new SKColor( + (byte)Math.Max(0, TextColor.Red - 40), + (byte)Math.Max(0, TextColor.Green - 40), + (byte)Math.Max(0, TextColor.Blue - 40), + TextColor.Alpha); + } + else + { + textColorToUse = TextColor; + } - protected override void OnEnabledChanged() - { - base.OnEnabledChanged(); - SkiaVisualStateManager.GoToState(this, base.IsEnabled ? "Normal" : "Disabled"); - } + using var paint = new SKPaint(font) + { + Color = textColorToUse, + IsAntialias = true + }; - protected override SKSize MeasureOverride(SKSize availableSize) - { - //IL_0001: Unknown result type (might be due to invalid IL or missing references) - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - //IL_0017: Unknown result type (might be due to invalid IL or missing references) - //IL_001c: Unknown result type (might be due to invalid IL or missing references) - //IL_002e: Unknown result type (might be due to invalid IL or missing references) - //IL_0033: Unknown result type (might be due to invalid IL or missing references) - //IL_0044: Unknown result type (might be due to invalid IL or missing references) - //IL_0049: Unknown result type (might be due to invalid IL or missing references) - //IL_005b: Unknown result type (might be due to invalid IL or missing references) - //IL_0060: Unknown result type (might be due to invalid IL or missing references) - //IL_0071: Unknown result type (might be due to invalid IL or missing references) - //IL_0076: Unknown result type (might be due to invalid IL or missing references) - //IL_0088: Unknown result type (might be due to invalid IL or missing references) - //IL_008d: Unknown result type (might be due to invalid IL or missing references) - //IL_009e: Unknown result type (might be due to invalid IL or missing references) - //IL_00a3: Unknown result type (might be due to invalid IL or missing references) - //IL_00f9: Unknown result type (might be due to invalid IL or missing references) - //IL_0120: Unknown result type (might be due to invalid IL or missing references) - //IL_0127: Expected O, but got Unknown - //IL_015a: Unknown result type (might be due to invalid IL or missing references) - //IL_0161: Expected O, but got Unknown - //IL_0163: Unknown result type (might be due to invalid IL or missing references) - //IL_016a: Expected O, but got Unknown - //IL_016c: Unknown result type (might be due to invalid IL or missing references) - //IL_0206: Unknown result type (might be due to invalid IL or missing references) - //IL_020b: Unknown result type (might be due to invalid IL or missing references) - //IL_0227: Unknown result type (might be due to invalid IL or missing references) - SKRect padding = Padding; - float num; - if (!float.IsNaN(((SKRect)(ref padding)).Left)) - { - padding = Padding; - num = ((SKRect)(ref padding)).Left; - } - else - { - num = 16f; - } - float num2 = num; - padding = Padding; - float num3; - if (!float.IsNaN(((SKRect)(ref padding)).Right)) - { - padding = Padding; - num3 = ((SKRect)(ref padding)).Right; - } - else - { - num3 = 16f; - } - float num4 = num3; - padding = Padding; - float num5; - if (!float.IsNaN(((SKRect)(ref padding)).Top)) - { - padding = Padding; - num5 = ((SKRect)(ref padding)).Top; - } - else - { - num5 = 8f; - } - float num6 = num5; - padding = Padding; - float num7; - if (!float.IsNaN(((SKRect)(ref padding)).Bottom)) - { - padding = Padding; - num7 = ((SKRect)(ref padding)).Bottom; - } - else - { - num7 = 8f; - } - float num8 = num7; - float num9 = ((float.IsNaN(FontSize) || FontSize <= 0f) ? 14f : FontSize); - if (string.IsNullOrEmpty(Text)) - { - return new SKSize(num2 + num4 + 40f, num6 + num8 + num9); - } - SKFontStyle style = new SKFontStyle((SKFontStyleWeight)(IsBold ? 700 : 400), (SKFontStyleWidth)5, (SKFontStyleSlant)(IsItalic ? 1 : 0)); - SKFont val = new SKFont(SkiaRenderingEngine.Current?.ResourceCache.GetTypeface(FontFamily, style) ?? SKTypeface.Default, num9, 1f, 0f); - try - { - SKPaint val2 = new SKPaint(val); - try - { - SKRect val3 = default(SKRect); - val2.MeasureText(Text, ref val3); - float num10 = ((SKRect)(ref val3)).Width + num2 + num4; - float num11 = ((SKRect)(ref val3)).Height + num6 + num8; - if (float.IsNaN(num10) || num10 < 0f) - { - num10 = 72f; - } - if (float.IsNaN(num11) || num11 < 0f) - { - num11 = 30f; - } - if (base.WidthRequest >= 0.0) - { - num10 = (float)base.WidthRequest; - } - if (base.HeightRequest >= 0.0) - { - num11 = (float)base.HeightRequest; - } - return new SKSize(num10, num11); - } - finally - { - ((IDisposable)val2)?.Dispose(); - } - } - finally - { - ((IDisposable)val)?.Dispose(); - } - } + // Measure text + var textBounds = new SKRect(); + paint.MeasureText(Text, ref textBounds); + + // Center text + var x = bounds.MidX - textBounds.MidX; + var y = bounds.MidY - textBounds.MidY; + + canvas.DrawText(Text, x, y, paint); + } + } + + private void DrawShadow(SKCanvas canvas, SKRect bounds) + { + using var shadowPaint = new SKPaint + { + Color = new SKColor(0, 0, 0, 50), + IsAntialias = true, + MaskFilter = SKMaskFilter.CreateBlur(SKBlurStyle.Normal, 4) + }; + + var shadowRect = new SKRect( + bounds.Left + 2, + bounds.Top + 4, + bounds.Right + 2, + bounds.Bottom + 4); + + var roundRect = new SKRoundRect(shadowRect, CornerRadius); + canvas.DrawRoundRect(roundRect, shadowPaint); + } + + public override void OnPointerEntered(PointerEventArgs e) + { + if (!IsEnabled) return; + IsHovered = true; + SkiaVisualStateManager.GoToState(this, SkiaVisualStateManager.CommonStates.PointerOver); + Invalidate(); + } + + public override void OnPointerExited(PointerEventArgs e) + { + IsHovered = false; + if (IsPressed) + { + IsPressed = false; + } + SkiaVisualStateManager.GoToState(this, IsEnabled ? SkiaVisualStateManager.CommonStates.Normal : SkiaVisualStateManager.CommonStates.Disabled); + Invalidate(); + } + + public override void OnPointerPressed(PointerEventArgs e) + { + Console.WriteLine($"[SkiaButton] OnPointerPressed - Text='{Text}', IsEnabled={IsEnabled}"); + if (!IsEnabled) return; + + IsPressed = true; + _focusFromKeyboard = false; + SkiaVisualStateManager.GoToState(this, SkiaVisualStateManager.CommonStates.Pressed); + Invalidate(); + Pressed?.Invoke(this, EventArgs.Empty); + } + + public override void OnPointerReleased(PointerEventArgs e) + { + if (!IsEnabled) return; + + var wasPressed = IsPressed; + IsPressed = false; + SkiaVisualStateManager.GoToState(this, IsHovered ? SkiaVisualStateManager.CommonStates.PointerOver : SkiaVisualStateManager.CommonStates.Normal); + Invalidate(); + + Released?.Invoke(this, EventArgs.Empty); + + // Fire click if button was pressed + // Note: Hit testing already verified the pointer is over this button, + // so we don't need to re-check bounds (which would fail due to coordinate system differences) + if (wasPressed) + { + Clicked?.Invoke(this, EventArgs.Empty); + Command?.Execute(CommandParameter); + } + } + + public override void OnKeyDown(KeyEventArgs e) + { + if (!IsEnabled) return; + + // Activate on Enter or Space + if (e.Key == Key.Enter || e.Key == Key.Space) + { + IsPressed = true; + _focusFromKeyboard = true; + SkiaVisualStateManager.GoToState(this, SkiaVisualStateManager.CommonStates.Pressed); + Invalidate(); + Pressed?.Invoke(this, EventArgs.Empty); + e.Handled = true; + } + } + + public override void OnKeyUp(KeyEventArgs e) + { + if (!IsEnabled) return; + + if (e.Key == Key.Enter || e.Key == Key.Space) + { + if (IsPressed) + { + IsPressed = false; + SkiaVisualStateManager.GoToState(this, SkiaVisualStateManager.CommonStates.Normal); + Invalidate(); + Released?.Invoke(this, EventArgs.Empty); + Clicked?.Invoke(this, EventArgs.Empty); + Command?.Execute(CommandParameter); + } + e.Handled = true; + } + } + + protected override void OnEnabledChanged() + { + base.OnEnabledChanged(); + SkiaVisualStateManager.GoToState(this, IsEnabled ? SkiaVisualStateManager.CommonStates.Normal : SkiaVisualStateManager.CommonStates.Disabled); + } + + protected override SKSize MeasureOverride(SKSize availableSize) + { + // Ensure we never return NaN - use safe defaults + var paddingLeft = float.IsNaN(Padding.Left) ? 16f : Padding.Left; + var paddingRight = float.IsNaN(Padding.Right) ? 16f : Padding.Right; + var paddingTop = float.IsNaN(Padding.Top) ? 8f : Padding.Top; + var paddingBottom = float.IsNaN(Padding.Bottom) ? 8f : Padding.Bottom; + var fontSize = float.IsNaN(FontSize) || FontSize <= 0 ? 14f : FontSize; + + if (string.IsNullOrEmpty(Text)) + { + return new SKSize( + paddingLeft + paddingRight + 40, // Minimum width + paddingTop + paddingBottom + fontSize); + } + + var fontStyle = new SKFontStyle( + IsBold ? SKFontStyleWeight.Bold : SKFontStyleWeight.Normal, + SKFontStyleWidth.Normal, + IsItalic ? SKFontStyleSlant.Italic : SKFontStyleSlant.Upright); + var typeface = SkiaRenderingEngine.Current?.ResourceCache.GetTypeface(FontFamily, fontStyle) + ?? SKTypeface.Default; + + using var font = new SKFont(typeface, fontSize); + using var paint = new SKPaint(font); + + var textBounds = new SKRect(); + paint.MeasureText(Text, ref textBounds); + + var width = textBounds.Width + paddingLeft + paddingRight; + var height = textBounds.Height + paddingTop + paddingBottom; + + // Ensure valid, non-NaN return values + if (float.IsNaN(width) || width < 0) width = 72f; + if (float.IsNaN(height) || height < 0) height = 30f; + + // Respect WidthRequest and HeightRequest when set + if (WidthRequest >= 0) + width = (float)WidthRequest; + if (HeightRequest >= 0) + height = (float)HeightRequest; + + return new SKSize(width, height); + } } diff --git a/Views/SkiaCarouselView.cs b/Views/SkiaCarouselView.cs index dcea97a..01012b2 100644 --- a/Views/SkiaCarouselView.cs +++ b/Views/SkiaCarouselView.cs @@ -1,381 +1,403 @@ -using System; -using System.Collections.Generic; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using SkiaSharp; namespace Microsoft.Maui.Platform; +/// +/// A horizontally scrolling carousel view with snap-to-item behavior. +/// public class SkiaCarouselView : SkiaLayoutView { - private readonly List _items = new List(); + private readonly List _items = new(); + private int _currentPosition = 0; + private float _scrollOffset = 0f; + private float _targetScrollOffset = 0f; + private bool _isDragging = false; + private float _dragStartX; + private float _dragStartOffset; + private float _velocity = 0f; + private DateTime _lastDragTime; + private float _lastDragX; - private int _currentPosition; + // Animation + private bool _isAnimating = false; + private float _animationStartOffset; + private float _animationTargetOffset; + private DateTime _animationStartTime; + private const float AnimationDurationMs = 300f; - private float _scrollOffset; + /// + /// Gets or sets the current position (item index). + /// + public int Position + { + get => _currentPosition; + set + { + if (value >= 0 && value < _items.Count && value != _currentPosition) + { + int oldPosition = _currentPosition; + _currentPosition = value; + AnimateToPosition(value); + PositionChanged?.Invoke(this, new PositionChangedEventArgs(oldPosition, value)); + } + } + } - private float _targetScrollOffset; + /// + /// Gets the item count. + /// + public int ItemCount => _items.Count; - private bool _isDragging; + /// + /// Gets or sets whether looping is enabled. + /// + public bool Loop { get; set; } = false; - private float _dragStartX; + /// + /// Gets or sets the peek amount (how much of adjacent items to show). + /// + public float PeekAreaInsets { get; set; } = 0f; - private float _dragStartOffset; + /// + /// Gets or sets the spacing between items. + /// + public float ItemSpacing { get; set; } = 0f; - private float _velocity; + /// + /// Gets or sets whether swipe gestures are enabled. + /// + public bool IsSwipeEnabled { get; set; } = true; - private DateTime _lastDragTime; + /// + /// Gets or sets the indicator visibility. + /// + public bool ShowIndicators { get; set; } = true; - private float _lastDragX; + /// + /// Gets or sets the indicator color. + /// + public SKColor IndicatorColor { get; set; } = new SKColor(180, 180, 180); - private bool _isAnimating; + /// + /// Gets or sets the selected indicator color. + /// + public SKColor SelectedIndicatorColor { get; set; } = new SKColor(33, 150, 243); - private float _animationStartOffset; + /// + /// Event raised when position changes. + /// + public event EventHandler? PositionChanged; - private float _animationTargetOffset; + /// + /// Event raised when scrolling. + /// + public event EventHandler? Scrolled; - private DateTime _animationStartTime; + /// + /// Adds an item to the carousel. + /// + public void AddItem(SkiaView item) + { + _items.Add(item); + AddChild(item); + InvalidateMeasure(); + Invalidate(); + } - private const float AnimationDurationMs = 300f; + /// + /// Removes an item from the carousel. + /// + public void RemoveItem(SkiaView item) + { + if (_items.Remove(item)) + { + RemoveChild(item); + if (_currentPosition >= _items.Count) + { + _currentPosition = Math.Max(0, _items.Count - 1); + } + InvalidateMeasure(); + Invalidate(); + } + } - public int Position - { - get - { - return _currentPosition; - } - set - { - if (value >= 0 && value < _items.Count && value != _currentPosition) - { - int currentPosition = _currentPosition; - _currentPosition = value; - AnimateToPosition(value); - this.PositionChanged?.Invoke(this, new PositionChangedEventArgs(currentPosition, value)); - } - } - } + /// + /// Clears all items. + /// + public void ClearItems() + { + foreach (var item in _items) + { + RemoveChild(item); + } + _items.Clear(); + _currentPosition = 0; + _scrollOffset = 0; + _targetScrollOffset = 0; + InvalidateMeasure(); + Invalidate(); + } - public int ItemCount => _items.Count; + /// + /// Scrolls to the specified position. + /// + public void ScrollTo(int position, bool animate = true) + { + if (position < 0 || position >= _items.Count) return; - public bool Loop { get; set; } + int oldPosition = _currentPosition; + _currentPosition = position; - public float PeekAreaInsets { get; set; } + if (animate) + { + AnimateToPosition(position); + } + else + { + _scrollOffset = GetOffsetForPosition(position); + _targetScrollOffset = _scrollOffset; + Invalidate(); + } - public float ItemSpacing { get; set; } + if (oldPosition != position) + { + PositionChanged?.Invoke(this, new PositionChangedEventArgs(oldPosition, position)); + } + } - public bool IsSwipeEnabled { get; set; } = true; + private void AnimateToPosition(int position) + { + _animationStartOffset = _scrollOffset; + _animationTargetOffset = GetOffsetForPosition(position); + _animationStartTime = DateTime.UtcNow; + _isAnimating = true; + Invalidate(); + } - public bool ShowIndicators { get; set; } = true; + private float GetOffsetForPosition(int position) + { + float itemWidth = Bounds.Width - PeekAreaInsets * 2; + return position * (itemWidth + ItemSpacing); + } - public SKColor IndicatorColor { get; set; } = new SKColor((byte)180, (byte)180, (byte)180); + private int GetPositionForOffset(float offset) + { + float itemWidth = Bounds.Width - PeekAreaInsets * 2; + if (itemWidth <= 0) return 0; + return Math.Clamp((int)Math.Round(offset / (itemWidth + ItemSpacing)), 0, Math.Max(0, _items.Count - 1)); + } - public SKColor SelectedIndicatorColor { get; set; } = new SKColor((byte)33, (byte)150, (byte)243); + protected override SKSize MeasureOverride(SKSize availableSize) + { + float itemWidth = availableSize.Width - PeekAreaInsets * 2; + float itemHeight = availableSize.Height - (ShowIndicators ? 30 : 0); - public event EventHandler? PositionChanged; + foreach (var item in _items) + { + item.Measure(new SKSize(itemWidth, itemHeight)); + } - public event EventHandler? Scrolled; + return availableSize; + } - public void AddItem(SkiaView item) - { - _items.Add(item); - AddChild(item); - InvalidateMeasure(); - Invalidate(); - } + protected override SKRect ArrangeOverride(SKRect bounds) + { + float itemWidth = bounds.Width - PeekAreaInsets * 2; + float itemHeight = bounds.Height - (ShowIndicators ? 30 : 0); - public void RemoveItem(SkiaView item) - { - if (_items.Remove(item)) - { - RemoveChild(item); - if (_currentPosition >= _items.Count) - { - _currentPosition = Math.Max(0, _items.Count - 1); - } - InvalidateMeasure(); - Invalidate(); - } - } + for (int i = 0; i < _items.Count; i++) + { + float x = bounds.Left + PeekAreaInsets + i * (itemWidth + ItemSpacing) - _scrollOffset; + var itemBounds = new SKRect(x, bounds.Top, x + itemWidth, bounds.Top + itemHeight); + _items[i].Arrange(itemBounds); + } - public void ClearItems() - { - foreach (SkiaView item in _items) - { - RemoveChild(item); - } - _items.Clear(); - _currentPosition = 0; - _scrollOffset = 0f; - _targetScrollOffset = 0f; - InvalidateMeasure(); - Invalidate(); - } + return bounds; + } - public void ScrollTo(int position, bool animate = true) - { - if (position >= 0 && position < _items.Count) - { - int currentPosition = _currentPosition; - _currentPosition = position; - if (animate) - { - AnimateToPosition(position); - } - else - { - _scrollOffset = GetOffsetForPosition(position); - _targetScrollOffset = _scrollOffset; - Invalidate(); - } - if (currentPosition != position) - { - this.PositionChanged?.Invoke(this, new PositionChangedEventArgs(currentPosition, position)); - } - } - } + protected override void OnDraw(SKCanvas canvas, SKRect bounds) + { + // Update animation + if (_isAnimating) + { + float elapsed = (float)(DateTime.UtcNow - _animationStartTime).TotalMilliseconds; + float progress = Math.Clamp(elapsed / AnimationDurationMs, 0f, 1f); - private void AnimateToPosition(int position) - { - _animationStartOffset = _scrollOffset; - _animationTargetOffset = GetOffsetForPosition(position); - _animationStartTime = DateTime.UtcNow; - _isAnimating = true; - Invalidate(); - } + // Ease out cubic + float t = 1f - (1f - progress) * (1f - progress) * (1f - progress); - private float GetOffsetForPosition(int position) - { - //IL_0001: Unknown result type (might be due to invalid IL or missing references) - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - SKRect bounds = base.Bounds; - float num = ((SKRect)(ref bounds)).Width - PeekAreaInsets * 2f; - return (float)position * (num + ItemSpacing); - } + _scrollOffset = _animationStartOffset + (_animationTargetOffset - _animationStartOffset) * t; - private int GetPositionForOffset(float offset) - { - //IL_0001: Unknown result type (might be due to invalid IL or missing references) - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - SKRect bounds = base.Bounds; - float num = ((SKRect)(ref bounds)).Width - PeekAreaInsets * 2f; - if (num <= 0f) - { - return 0; - } - return Math.Clamp((int)Math.Round(offset / (num + ItemSpacing)), 0, Math.Max(0, _items.Count - 1)); - } + if (progress >= 1f) + { + _isAnimating = false; + _scrollOffset = _animationTargetOffset; + } + else + { + Invalidate(); // Continue animation + } + } - protected override SKSize MeasureOverride(SKSize availableSize) - { - //IL_0043: Unknown result type (might be due to invalid IL or missing references) - //IL_0048: Unknown result type (might be due to invalid IL or missing references) - //IL_0067: Unknown result type (might be due to invalid IL or missing references) - float num = ((SKSize)(ref availableSize)).Width - PeekAreaInsets * 2f; - float num2 = ((SKSize)(ref availableSize)).Height - (float)(ShowIndicators ? 30 : 0); - foreach (SkiaView item in _items) - { - item.Measure(new SKSize(num, num2)); - } - return availableSize; - } + canvas.Save(); + canvas.ClipRect(bounds); - protected override SKRect ArrangeOverride(SKRect bounds) - { - //IL_0079: Unknown result type (might be due to invalid IL or missing references) - //IL_0092: Unknown result type (might be due to invalid IL or missing references) - float num = ((SKRect)(ref bounds)).Width - PeekAreaInsets * 2f; - float num2 = ((SKRect)(ref bounds)).Height - (float)(ShowIndicators ? 30 : 0); - SKRect bounds2 = default(SKRect); - for (int i = 0; i < _items.Count; i++) - { - float num3 = ((SKRect)(ref bounds)).Left + PeekAreaInsets + (float)i * (num + ItemSpacing) - _scrollOffset; - ((SKRect)(ref bounds2))._002Ector(num3, ((SKRect)(ref bounds)).Top, num3 + num, ((SKRect)(ref bounds)).Top + num2); - _items[i].Arrange(bounds2); - } - return bounds; - } + // Draw visible items + float itemWidth = bounds.Width - PeekAreaInsets * 2; + float contentHeight = bounds.Height - (ShowIndicators ? 30 : 0); - protected override void OnDraw(SKCanvas canvas, SKRect bounds) - { - //IL_009f: Unknown result type (might be due to invalid IL or missing references) - //IL_014c: Unknown result type (might be due to invalid IL or missing references) - if (_isAnimating) - { - float num = Math.Clamp((float)(DateTime.UtcNow - _animationStartTime).TotalMilliseconds / 300f, 0f, 1f); - float num2 = 1f - (1f - num) * (1f - num) * (1f - num); - _scrollOffset = _animationStartOffset + (_animationTargetOffset - _animationStartOffset) * num2; - if (num >= 1f) - { - _isAnimating = false; - _scrollOffset = _animationTargetOffset; - } - else - { - Invalidate(); - } - } - canvas.Save(); - canvas.ClipRect(bounds, (SKClipOperation)1, false); - float num3 = ((SKRect)(ref bounds)).Width - PeekAreaInsets * 2f; - _ = ((SKRect)(ref bounds)).Height; - _ = ShowIndicators; - for (int i = 0; i < _items.Count; i++) - { - float num4 = ((SKRect)(ref bounds)).Left + PeekAreaInsets + (float)i * (num3 + ItemSpacing) - _scrollOffset; - if (num4 + num3 > ((SKRect)(ref bounds)).Left && num4 < ((SKRect)(ref bounds)).Right) - { - _items[i].Draw(canvas); - } - } - if (ShowIndicators && _items.Count > 1) - { - DrawIndicators(canvas, bounds); - } - canvas.Restore(); - } + for (int i = 0; i < _items.Count; i++) + { + float x = bounds.Left + PeekAreaInsets + i * (itemWidth + ItemSpacing) - _scrollOffset; - private void DrawIndicators(SKCanvas canvas, SKRect bounds) - { - //IL_004d: Unknown result type (might be due to invalid IL or missing references) - //IL_0052: Unknown result type (might be due to invalid IL or missing references) - //IL_0054: Unknown result type (might be due to invalid IL or missing references) - //IL_005e: Unknown result type (might be due to invalid IL or missing references) - //IL_0065: Unknown result type (might be due to invalid IL or missing references) - //IL_006e: Expected O, but got Unknown - //IL_006e: Unknown result type (might be due to invalid IL or missing references) - //IL_0073: Unknown result type (might be due to invalid IL or missing references) - //IL_0075: Unknown result type (might be due to invalid IL or missing references) - //IL_007f: Unknown result type (might be due to invalid IL or missing references) - //IL_0086: Unknown result type (might be due to invalid IL or missing references) - //IL_008f: Expected O, but got Unknown - float num = 8f; - float num2 = 12f; - float num3 = (float)_items.Count * num + (float)(_items.Count - 1) * (num2 - num); - float num4 = ((SKRect)(ref bounds)).MidX - num3 / 2f; - float num5 = ((SKRect)(ref bounds)).Bottom - 15f; - SKPaint val = new SKPaint - { - Color = IndicatorColor, - Style = (SKPaintStyle)0, - IsAntialias = true - }; - try - { - SKPaint val2 = new SKPaint - { - Color = SelectedIndicatorColor, - Style = (SKPaintStyle)0, - IsAntialias = true - }; - try - { - for (int i = 0; i < _items.Count; i++) - { - float num6 = num4 + (float)i * num2; - SKPaint val3 = ((i == _currentPosition) ? val2 : val); - canvas.DrawCircle(num6, num5, num / 2f, val3); - } - } - finally - { - ((IDisposable)val2)?.Dispose(); - } - } - finally - { - ((IDisposable)val)?.Dispose(); - } - } + // Only draw visible items + if (x + itemWidth > bounds.Left && x < bounds.Right) + { + _items[i].Draw(canvas); + } + } - public override SkiaView? HitTest(float x, float y) - { - //IL_0009: Unknown result type (might be due to invalid IL or missing references) - //IL_000e: Unknown result type (might be due to invalid IL or missing references) - if (base.IsVisible) - { - SKRect bounds = base.Bounds; - if (((SKRect)(ref bounds)).Contains(x, y)) - { - { - foreach (SkiaView item in _items) - { - SkiaView skiaView = item.HitTest(x, y); - if (skiaView != null) - { - return skiaView; - } - } - return this; - } - } - } - return null; - } + // Draw indicators + if (ShowIndicators && _items.Count > 1) + { + DrawIndicators(canvas, bounds); + } - public override void OnPointerPressed(PointerEventArgs e) - { - if (base.IsEnabled && IsSwipeEnabled) - { - _isDragging = true; - _dragStartX = e.X; - _dragStartOffset = _scrollOffset; - _lastDragX = e.X; - _lastDragTime = DateTime.UtcNow; - _velocity = 0f; - _isAnimating = false; - e.Handled = true; - base.OnPointerPressed(e); - } - } + canvas.Restore(); + } - public override void OnPointerMoved(PointerEventArgs e) - { - if (_isDragging) - { - float num = _dragStartX - e.X; - _scrollOffset = _dragStartOffset + num; - float offsetForPosition = GetOffsetForPosition(_items.Count - 1); - _scrollOffset = Math.Clamp(_scrollOffset, 0f, offsetForPosition); - DateTime utcNow = DateTime.UtcNow; - float num2 = (float)(utcNow - _lastDragTime).TotalSeconds; - if (num2 > 0f) - { - _velocity = (_lastDragX - e.X) / num2; - } - _lastDragX = e.X; - _lastDragTime = utcNow; - this.Scrolled?.Invoke(this, EventArgs.Empty); - Invalidate(); - e.Handled = true; - base.OnPointerMoved(e); - } - } + private void DrawIndicators(SKCanvas canvas, SKRect bounds) + { + float indicatorSize = 8f; + float indicatorSpacing = 12f; + float totalWidth = _items.Count * indicatorSize + (_items.Count - 1) * (indicatorSpacing - indicatorSize); + float startX = bounds.MidX - totalWidth / 2; + float y = bounds.Bottom - 15; - public override void OnPointerReleased(PointerEventArgs e) - { - //IL_0011: Unknown result type (might be due to invalid IL or missing references) - //IL_0016: Unknown result type (might be due to invalid IL or missing references) - if (!_isDragging) - { - return; - } - _isDragging = false; - SKRect bounds = base.Bounds; - _ = ((SKRect)(ref bounds)).Width; - _ = PeekAreaInsets; - int num = GetPositionForOffset(_scrollOffset); - if (Math.Abs(_velocity) > 500f) - { - if (_velocity > 0f && num < _items.Count - 1) - { - num++; - } - else if (_velocity < 0f && num > 0) - { - num--; - } - } - ScrollTo(num); - e.Handled = true; - base.OnPointerReleased(e); - } + using var normalPaint = new SKPaint + { + Color = IndicatorColor, + Style = SKPaintStyle.Fill, + IsAntialias = true + }; + + using var selectedPaint = new SKPaint + { + Color = SelectedIndicatorColor, + Style = SKPaintStyle.Fill, + IsAntialias = true + }; + + for (int i = 0; i < _items.Count; i++) + { + float x = startX + i * indicatorSpacing; + var paint = i == _currentPosition ? selectedPaint : normalPaint; + canvas.DrawCircle(x, y, indicatorSize / 2, paint); + } + } + + public override SkiaView? HitTest(float x, float y) + { + if (!IsVisible || !Bounds.Contains(x, y)) return null; + + // Check items + foreach (var item in _items) + { + var hit = item.HitTest(x, y); + if (hit != null) return hit; + } + + return this; + } + + public override void OnPointerPressed(PointerEventArgs e) + { + if (!IsEnabled || !IsSwipeEnabled) return; + + _isDragging = true; + _dragStartX = e.X; + _dragStartOffset = _scrollOffset; + _lastDragX = e.X; + _lastDragTime = DateTime.UtcNow; + _velocity = 0; + _isAnimating = false; + + e.Handled = true; + base.OnPointerPressed(e); + } + + public override void OnPointerMoved(PointerEventArgs e) + { + if (!_isDragging) return; + + float delta = _dragStartX - e.X; + _scrollOffset = _dragStartOffset + delta; + + // Clamp scrolling + float maxOffset = GetOffsetForPosition(_items.Count - 1); + _scrollOffset = Math.Clamp(_scrollOffset, 0, maxOffset); + + // Calculate velocity + var now = DateTime.UtcNow; + float timeDelta = (float)(now - _lastDragTime).TotalSeconds; + if (timeDelta > 0) + { + _velocity = (_lastDragX - e.X) / timeDelta; + } + _lastDragX = e.X; + _lastDragTime = now; + + Scrolled?.Invoke(this, EventArgs.Empty); + Invalidate(); + e.Handled = true; + + base.OnPointerMoved(e); + } + + public override void OnPointerReleased(PointerEventArgs e) + { + if (!_isDragging) return; + + _isDragging = false; + + // Determine target position based on velocity and position + float itemWidth = Bounds.Width - PeekAreaInsets * 2; + int targetPosition = GetPositionForOffset(_scrollOffset); + + // Apply velocity influence + if (Math.Abs(_velocity) > 500) + { + if (_velocity > 0 && targetPosition < _items.Count - 1) + { + targetPosition++; + } + else if (_velocity < 0 && targetPosition > 0) + { + targetPosition--; + } + } + + ScrollTo(targetPosition, true); + e.Handled = true; + + base.OnPointerReleased(e); + } +} + +/// +/// Event args for position changed events. +/// +public class PositionChangedEventArgs : EventArgs +{ + public int PreviousPosition { get; } + public int CurrentPosition { get; } + + public PositionChangedEventArgs(int previousPosition, int currentPosition) + { + PreviousPosition = previousPosition; + CurrentPosition = currentPosition; + } } diff --git a/Views/SkiaCheckBox.cs b/Views/SkiaCheckBox.cs index 9065ceb..7fddd92 100644 --- a/Views/SkiaCheckBox.cs +++ b/Views/SkiaCheckBox.cs @@ -1,454 +1,413 @@ -using System; -using System.Runtime.CompilerServices; -using Microsoft.Maui.Controls; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using SkiaSharp; +using Microsoft.Maui.Platform.Linux.Rendering; namespace Microsoft.Maui.Platform; +/// +/// Skia-rendered checkbox control with full XAML styling support. +/// public class SkiaCheckBox : SkiaView { - public static readonly BindableProperty IsCheckedProperty = BindableProperty.Create("IsChecked", typeof(bool), typeof(SkiaCheckBox), (object)false, (BindingMode)1, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaCheckBox)(object)b).OnIsCheckedChanged(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + #region BindableProperties - public static readonly BindableProperty CheckColorProperty = BindableProperty.Create("CheckColor", typeof(SKColor), typeof(SkiaCheckBox), (object)SKColors.White, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaCheckBox)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for IsChecked. + /// + public static readonly BindableProperty IsCheckedProperty = + BindableProperty.Create( + nameof(IsChecked), + typeof(bool), + typeof(SkiaCheckBox), + false, + BindingMode.TwoWay, + propertyChanged: (b, o, n) => ((SkiaCheckBox)b).OnIsCheckedChanged()); - public static readonly BindableProperty BoxColorProperty = BindableProperty.Create("BoxColor", typeof(SKColor), typeof(SkiaCheckBox), (object)new SKColor((byte)33, (byte)150, (byte)243), (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaCheckBox)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for CheckColor. + /// + public static readonly BindableProperty CheckColorProperty = + BindableProperty.Create( + nameof(CheckColor), + typeof(SKColor), + typeof(SkiaCheckBox), + SKColors.White, + propertyChanged: (b, o, n) => ((SkiaCheckBox)b).Invalidate()); - public static readonly BindableProperty UncheckedBoxColorProperty = BindableProperty.Create("UncheckedBoxColor", typeof(SKColor), typeof(SkiaCheckBox), (object)SKColors.White, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaCheckBox)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for BoxColor. + /// + public static readonly BindableProperty BoxColorProperty = + BindableProperty.Create( + nameof(BoxColor), + typeof(SKColor), + typeof(SkiaCheckBox), + new SKColor(0x21, 0x96, 0xF3), + propertyChanged: (b, o, n) => ((SkiaCheckBox)b).Invalidate()); - public static readonly BindableProperty BorderColorProperty = BindableProperty.Create("BorderColor", typeof(SKColor), typeof(SkiaCheckBox), (object)new SKColor((byte)117, (byte)117, (byte)117), (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaCheckBox)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for UncheckedBoxColor. + /// + public static readonly BindableProperty UncheckedBoxColorProperty = + BindableProperty.Create( + nameof(UncheckedBoxColor), + typeof(SKColor), + typeof(SkiaCheckBox), + SKColors.White, + propertyChanged: (b, o, n) => ((SkiaCheckBox)b).Invalidate()); - public static readonly BindableProperty DisabledColorProperty = BindableProperty.Create("DisabledColor", typeof(SKColor), typeof(SkiaCheckBox), (object)new SKColor((byte)189, (byte)189, (byte)189), (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaCheckBox)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for BorderColor. + /// + public static readonly BindableProperty BorderColorProperty = + BindableProperty.Create( + nameof(BorderColor), + typeof(SKColor), + typeof(SkiaCheckBox), + new SKColor(0x75, 0x75, 0x75), + propertyChanged: (b, o, n) => ((SkiaCheckBox)b).Invalidate()); - public static readonly BindableProperty HoveredBorderColorProperty = BindableProperty.Create("HoveredBorderColor", typeof(SKColor), typeof(SkiaCheckBox), (object)new SKColor((byte)33, (byte)150, (byte)243), (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaCheckBox)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for DisabledColor. + /// + public static readonly BindableProperty DisabledColorProperty = + BindableProperty.Create( + nameof(DisabledColor), + typeof(SKColor), + typeof(SkiaCheckBox), + new SKColor(0xBD, 0xBD, 0xBD), + propertyChanged: (b, o, n) => ((SkiaCheckBox)b).Invalidate()); - public static readonly BindableProperty BoxSizeProperty = BindableProperty.Create("BoxSize", typeof(float), typeof(SkiaCheckBox), (object)20f, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaCheckBox)(object)b).InvalidateMeasure(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for HoveredBorderColor. + /// + public static readonly BindableProperty HoveredBorderColorProperty = + BindableProperty.Create( + nameof(HoveredBorderColor), + typeof(SKColor), + typeof(SkiaCheckBox), + new SKColor(0x21, 0x96, 0xF3), + propertyChanged: (b, o, n) => ((SkiaCheckBox)b).Invalidate()); - public static readonly BindableProperty CornerRadiusProperty = BindableProperty.Create("CornerRadius", typeof(float), typeof(SkiaCheckBox), (object)3f, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaCheckBox)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for BoxSize. + /// + public static readonly BindableProperty BoxSizeProperty = + BindableProperty.Create( + nameof(BoxSize), + typeof(float), + typeof(SkiaCheckBox), + 20f, + propertyChanged: (b, o, n) => ((SkiaCheckBox)b).InvalidateMeasure()); - public static readonly BindableProperty BorderWidthProperty = BindableProperty.Create("BorderWidth", typeof(float), typeof(SkiaCheckBox), (object)2f, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaCheckBox)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for CornerRadius. + /// + public static readonly BindableProperty CornerRadiusProperty = + BindableProperty.Create( + nameof(CornerRadius), + typeof(float), + typeof(SkiaCheckBox), + 3f, + propertyChanged: (b, o, n) => ((SkiaCheckBox)b).Invalidate()); - public static readonly BindableProperty CheckStrokeWidthProperty = BindableProperty.Create("CheckStrokeWidth", typeof(float), typeof(SkiaCheckBox), (object)2.5f, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaCheckBox)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for BorderWidth. + /// + public static readonly BindableProperty BorderWidthProperty = + BindableProperty.Create( + nameof(BorderWidth), + typeof(float), + typeof(SkiaCheckBox), + 2f, + propertyChanged: (b, o, n) => ((SkiaCheckBox)b).Invalidate()); - public bool IsChecked - { - get - { - return (bool)((BindableObject)this).GetValue(IsCheckedProperty); - } - set - { - ((BindableObject)this).SetValue(IsCheckedProperty, (object)value); - } - } + /// + /// Bindable property for CheckStrokeWidth. + /// + public static readonly BindableProperty CheckStrokeWidthProperty = + BindableProperty.Create( + nameof(CheckStrokeWidth), + typeof(float), + typeof(SkiaCheckBox), + 2.5f, + propertyChanged: (b, o, n) => ((SkiaCheckBox)b).Invalidate()); - public SKColor CheckColor - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKColor)((BindableObject)this).GetValue(CheckColorProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(CheckColorProperty, (object)value); - } - } + #endregion - public SKColor BoxColor - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKColor)((BindableObject)this).GetValue(BoxColorProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(BoxColorProperty, (object)value); - } - } + #region Properties - public SKColor UncheckedBoxColor - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKColor)((BindableObject)this).GetValue(UncheckedBoxColorProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(UncheckedBoxColorProperty, (object)value); - } - } + /// + /// Gets or sets whether the checkbox is checked. + /// + public bool IsChecked + { + get => (bool)GetValue(IsCheckedProperty); + set => SetValue(IsCheckedProperty, value); + } - public SKColor BorderColor - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKColor)((BindableObject)this).GetValue(BorderColorProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(BorderColorProperty, (object)value); - } - } + /// + /// Gets or sets the check color. + /// + public SKColor CheckColor + { + get => (SKColor)GetValue(CheckColorProperty); + set => SetValue(CheckColorProperty, value); + } - public SKColor DisabledColor - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKColor)((BindableObject)this).GetValue(DisabledColorProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(DisabledColorProperty, (object)value); - } - } + /// + /// Gets or sets the box color when checked. + /// + public SKColor BoxColor + { + get => (SKColor)GetValue(BoxColorProperty); + set => SetValue(BoxColorProperty, value); + } - public SKColor HoveredBorderColor - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKColor)((BindableObject)this).GetValue(HoveredBorderColorProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(HoveredBorderColorProperty, (object)value); - } - } + /// + /// Gets or sets the box color when unchecked. + /// + public SKColor UncheckedBoxColor + { + get => (SKColor)GetValue(UncheckedBoxColorProperty); + set => SetValue(UncheckedBoxColorProperty, value); + } - public float BoxSize - { - get - { - return (float)((BindableObject)this).GetValue(BoxSizeProperty); - } - set - { - ((BindableObject)this).SetValue(BoxSizeProperty, (object)value); - } - } + /// + /// Gets or sets the border color. + /// + public SKColor BorderColor + { + get => (SKColor)GetValue(BorderColorProperty); + set => SetValue(BorderColorProperty, value); + } - public float CornerRadius - { - get - { - return (float)((BindableObject)this).GetValue(CornerRadiusProperty); - } - set - { - ((BindableObject)this).SetValue(CornerRadiusProperty, (object)value); - } - } + /// + /// Gets or sets the disabled color. + /// + public SKColor DisabledColor + { + get => (SKColor)GetValue(DisabledColorProperty); + set => SetValue(DisabledColorProperty, value); + } - public float BorderWidth - { - get - { - return (float)((BindableObject)this).GetValue(BorderWidthProperty); - } - set - { - ((BindableObject)this).SetValue(BorderWidthProperty, (object)value); - } - } + /// + /// Gets or sets the hovered border color. + /// + public SKColor HoveredBorderColor + { + get => (SKColor)GetValue(HoveredBorderColorProperty); + set => SetValue(HoveredBorderColorProperty, value); + } - public float CheckStrokeWidth - { - get - { - return (float)((BindableObject)this).GetValue(CheckStrokeWidthProperty); - } - set - { - ((BindableObject)this).SetValue(CheckStrokeWidthProperty, (object)value); - } - } + /// + /// Gets or sets the box size. + /// + public float BoxSize + { + get => (float)GetValue(BoxSizeProperty); + set => SetValue(BoxSizeProperty, value); + } - public bool IsHovered { get; private set; } + /// + /// Gets or sets the corner radius. + /// + public float CornerRadius + { + get => (float)GetValue(CornerRadiusProperty); + set => SetValue(CornerRadiusProperty, value); + } - public event EventHandler? CheckedChanged; + /// + /// Gets or sets the border width. + /// + public float BorderWidth + { + get => (float)GetValue(BorderWidthProperty); + set => SetValue(BorderWidthProperty, value); + } - public SkiaCheckBox() - { - base.IsFocusable = true; - } + /// + /// Gets or sets the check stroke width. + /// + public float CheckStrokeWidth + { + get => (float)GetValue(CheckStrokeWidthProperty); + set => SetValue(CheckStrokeWidthProperty, value); + } - private void OnIsCheckedChanged() - { - this.CheckedChanged?.Invoke(this, new CheckedChangedEventArgs(IsChecked)); - SkiaVisualStateManager.GoToState(this, IsChecked ? "Checked" : "Unchecked"); - Invalidate(); - } + /// + /// Gets whether the pointer is over the checkbox. + /// + public bool IsHovered { get; private set; } - protected override void OnDraw(SKCanvas canvas, SKRect bounds) - { - //IL_0085: Unknown result type (might be due to invalid IL or missing references) - //IL_008c: Unknown result type (might be due to invalid IL or missing references) - //IL_0092: Expected O, but got Unknown - //IL_018b: Unknown result type (might be due to invalid IL or missing references) - //IL_0190: Unknown result type (might be due to invalid IL or missing references) - //IL_00b6: Unknown result type (might be due to invalid IL or missing references) - //IL_00bb: Unknown result type (might be due to invalid IL or missing references) - //IL_00d8: Unknown result type (might be due to invalid IL or missing references) - //IL_00dd: Unknown result type (might be due to invalid IL or missing references) - //IL_00fa: Unknown result type (might be due to invalid IL or missing references) - //IL_00ff: Unknown result type (might be due to invalid IL or missing references) - //IL_011c: Unknown result type (might be due to invalid IL or missing references) - //IL_0121: Unknown result type (might be due to invalid IL or missing references) - //IL_013e: Unknown result type (might be due to invalid IL or missing references) - //IL_0143: Unknown result type (might be due to invalid IL or missing references) - //IL_0160: Unknown result type (might be due to invalid IL or missing references) - //IL_0165: Unknown result type (might be due to invalid IL or missing references) - //IL_01b2: Unknown result type (might be due to invalid IL or missing references) - //IL_01bc: Unknown result type (might be due to invalid IL or missing references) - //IL_01c3: Unknown result type (might be due to invalid IL or missing references) - //IL_01cb: Expected O, but got Unknown - //IL_01aa: Unknown result type (might be due to invalid IL or missing references) - //IL_01a2: Unknown result type (might be due to invalid IL or missing references) - //IL_01d3: Unknown result type (might be due to invalid IL or missing references) - //IL_01d8: Unknown result type (might be due to invalid IL or missing references) - //IL_020a: Unknown result type (might be due to invalid IL or missing references) - //IL_0214: Unknown result type (might be due to invalid IL or missing references) - //IL_021b: Unknown result type (might be due to invalid IL or missing references) - //IL_0222: Unknown result type (might be due to invalid IL or missing references) - //IL_022f: Expected O, but got Unknown - //IL_0202: Unknown result type (might be due to invalid IL or missing references) - //IL_01fa: Unknown result type (might be due to invalid IL or missing references) - //IL_01f2: Unknown result type (might be due to invalid IL or missing references) - //IL_023f: Unknown result type (might be due to invalid IL or missing references) - //IL_0244: Unknown result type (might be due to invalid IL or missing references) - //IL_0246: Unknown result type (might be due to invalid IL or missing references) - //IL_024b: Unknown result type (might be due to invalid IL or missing references) - //IL_0251: Unknown result type (might be due to invalid IL or missing references) - //IL_025b: Unknown result type (might be due to invalid IL or missing references) - //IL_0262: Unknown result type (might be due to invalid IL or missing references) - //IL_0269: Unknown result type (might be due to invalid IL or missing references) - //IL_0276: Expected O, but got Unknown - //IL_02b7: Unknown result type (might be due to invalid IL or missing references) - //IL_0276: Unknown result type (might be due to invalid IL or missing references) - //IL_027d: Unknown result type (might be due to invalid IL or missing references) - //IL_0284: Expected O, but got Unknown - SKRect val = default(SKRect); - ((SKRect)(ref val))._002Ector(((SKRect)(ref bounds)).Left + (((SKRect)(ref bounds)).Width - BoxSize) / 2f, ((SKRect)(ref bounds)).Top + (((SKRect)(ref bounds)).Height - BoxSize) / 2f, ((SKRect)(ref bounds)).Left + (((SKRect)(ref bounds)).Width - BoxSize) / 2f + BoxSize, ((SKRect)(ref bounds)).Top + (((SKRect)(ref bounds)).Height - BoxSize) / 2f + BoxSize); - SKRoundRect val2 = new SKRoundRect(val, CornerRadius); - SKColor val3; - if (IsChecked) - { - DefaultInterpolatedStringHandler defaultInterpolatedStringHandler = new DefaultInterpolatedStringHandler(69, 6); - defaultInterpolatedStringHandler.AppendLiteral("[SkiaCheckBox] OnDraw CHECKED - BoxColor=("); - val3 = BoxColor; - defaultInterpolatedStringHandler.AppendFormatted(((SKColor)(ref val3)).Red); - defaultInterpolatedStringHandler.AppendLiteral(","); - val3 = BoxColor; - defaultInterpolatedStringHandler.AppendFormatted(((SKColor)(ref val3)).Green); - defaultInterpolatedStringHandler.AppendLiteral(","); - val3 = BoxColor; - defaultInterpolatedStringHandler.AppendFormatted(((SKColor)(ref val3)).Blue); - defaultInterpolatedStringHandler.AppendLiteral("), UncheckedBoxColor=("); - val3 = UncheckedBoxColor; - defaultInterpolatedStringHandler.AppendFormatted(((SKColor)(ref val3)).Red); - defaultInterpolatedStringHandler.AppendLiteral(","); - val3 = UncheckedBoxColor; - defaultInterpolatedStringHandler.AppendFormatted(((SKColor)(ref val3)).Green); - defaultInterpolatedStringHandler.AppendLiteral(","); - val3 = UncheckedBoxColor; - defaultInterpolatedStringHandler.AppendFormatted(((SKColor)(ref val3)).Blue); - defaultInterpolatedStringHandler.AppendLiteral(")"); - Console.WriteLine(defaultInterpolatedStringHandler.ToStringAndClear()); - } - SKPaint val4 = new SKPaint - { - Color = ((!base.IsEnabled) ? DisabledColor : (IsChecked ? BoxColor : UncheckedBoxColor)), - IsAntialias = true, - Style = (SKPaintStyle)0 - }; - try - { - canvas.DrawRoundRect(val2, val4); - SKPaint val5 = new SKPaint - { - Color = ((!base.IsEnabled) ? DisabledColor : (IsChecked ? BoxColor : (IsHovered ? HoveredBorderColor : BorderColor))), - IsAntialias = true, - Style = (SKPaintStyle)1, - StrokeWidth = BorderWidth - }; - try - { - canvas.DrawRoundRect(val2, val5); - if (base.IsFocused) - { - SKPaint val6 = new SKPaint(); - val3 = BoxColor; - val6.Color = ((SKColor)(ref val3)).WithAlpha((byte)80); - val6.IsAntialias = true; - val6.Style = (SKPaintStyle)1; - val6.StrokeWidth = 3f; - SKPaint val7 = val6; - try - { - SKRoundRect val8 = new SKRoundRect(val, CornerRadius); - val8.Inflate(4f, 4f); - canvas.DrawRoundRect(val8, val7); - } - finally - { - ((IDisposable)val7)?.Dispose(); - } - } - if (IsChecked) - { - DrawCheckmark(canvas, val); - } - } - finally - { - ((IDisposable)val5)?.Dispose(); - } - } - finally - { - ((IDisposable)val4)?.Dispose(); - } - } + #endregion - private void DrawCheckmark(SKCanvas canvas, SKRect boxRect) - { - //IL_0000: Unknown result type (might be due to invalid IL or missing references) - //IL_0005: Unknown result type (might be due to invalid IL or missing references) - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - //IL_0010: Unknown result type (might be due to invalid IL or missing references) - //IL_0017: Unknown result type (might be due to invalid IL or missing references) - //IL_001e: Unknown result type (might be due to invalid IL or missing references) - //IL_002a: Unknown result type (might be due to invalid IL or missing references) - //IL_0031: Unknown result type (might be due to invalid IL or missing references) - //IL_0039: Expected O, but got Unknown - //IL_0070: Unknown result type (might be due to invalid IL or missing references) - //IL_0077: Expected O, but got Unknown - SKPaint val = new SKPaint - { - Color = SKColors.White, - IsAntialias = true, - Style = (SKPaintStyle)1, - StrokeWidth = CheckStrokeWidth, - StrokeCap = (SKStrokeCap)1, - StrokeJoin = (SKStrokeJoin)1 - }; - try - { - float num = BoxSize * 0.2f; - float num2 = ((SKRect)(ref boxRect)).Left + num; - float num3 = ((SKRect)(ref boxRect)).Right - num; - float num4 = ((SKRect)(ref boxRect)).Top + num; - float num5 = ((SKRect)(ref boxRect)).Bottom - num; - SKPath val2 = new SKPath(); - try - { - val2.MoveTo(num2, ((SKRect)(ref boxRect)).MidY); - val2.LineTo(((SKRect)(ref boxRect)).MidX - num * 0.3f, num5 - num * 0.5f); - val2.LineTo(num3, num4 + num * 0.3f); - canvas.DrawPath(val2, val); - } - finally - { - ((IDisposable)val2)?.Dispose(); - } - } - finally - { - ((IDisposable)val)?.Dispose(); - } - } + /// + /// Event raised when checked state changes. + /// + public event EventHandler? CheckedChanged; - public override void OnPointerEntered(PointerEventArgs e) - { - if (base.IsEnabled) - { - IsHovered = true; - SkiaVisualStateManager.GoToState(this, "PointerOver"); - Invalidate(); - } - } + public SkiaCheckBox() + { + IsFocusable = true; + } - public override void OnPointerExited(PointerEventArgs e) - { - IsHovered = false; - SkiaVisualStateManager.GoToState(this, base.IsEnabled ? "Normal" : "Disabled"); - Invalidate(); - } + private void OnIsCheckedChanged() + { + CheckedChanged?.Invoke(this, new CheckedChangedEventArgs(IsChecked)); + SkiaVisualStateManager.GoToState(this, IsChecked ? SkiaVisualStateManager.CommonStates.Checked : SkiaVisualStateManager.CommonStates.Unchecked); + Invalidate(); + } - public override void OnPointerPressed(PointerEventArgs e) - { - if (base.IsEnabled) - { - IsChecked = !IsChecked; - e.Handled = true; - } - } + protected override void OnDraw(SKCanvas canvas, SKRect bounds) + { + // Center the checkbox box in bounds + var boxRect = new SKRect( + bounds.Left + (bounds.Width - BoxSize) / 2, + bounds.Top + (bounds.Height - BoxSize) / 2, + bounds.Left + (bounds.Width - BoxSize) / 2 + BoxSize, + bounds.Top + (bounds.Height - BoxSize) / 2 + BoxSize); - public override void OnPointerReleased(PointerEventArgs e) - { - } + var roundRect = new SKRoundRect(boxRect, CornerRadius); - public override void OnKeyDown(KeyEventArgs e) - { - if (base.IsEnabled && e.Key == Key.Space) - { - IsChecked = !IsChecked; - e.Handled = true; - } - } + // Draw background + using var bgPaint = new SKPaint + { + Color = !IsEnabled ? DisabledColor + : IsChecked ? BoxColor + : UncheckedBoxColor, + IsAntialias = true, + Style = SKPaintStyle.Fill + }; + canvas.DrawRoundRect(roundRect, bgPaint); - protected override void OnEnabledChanged() - { - base.OnEnabledChanged(); - SkiaVisualStateManager.GoToState(this, base.IsEnabled ? "Normal" : "Disabled"); - } + // Draw border + using var borderPaint = new SKPaint + { + Color = !IsEnabled ? DisabledColor + : IsChecked ? BoxColor + : IsHovered ? HoveredBorderColor + : BorderColor, + IsAntialias = true, + Style = SKPaintStyle.Stroke, + StrokeWidth = BorderWidth + }; + canvas.DrawRoundRect(roundRect, borderPaint); - protected override SKSize MeasureOverride(SKSize availableSize) - { - //IL_0018: Unknown result type (might be due to invalid IL or missing references) - return new SKSize(BoxSize + 8f, BoxSize + 8f); - } + // Draw focus ring + if (IsFocused) + { + using var focusPaint = new SKPaint + { + Color = BoxColor.WithAlpha(80), + IsAntialias = true, + Style = SKPaintStyle.Stroke, + StrokeWidth = 3 + }; + var focusRect = new SKRoundRect(boxRect, CornerRadius); + focusRect.Inflate(4, 4); + canvas.DrawRoundRect(focusRect, focusPaint); + } + + // Draw checkmark + if (IsChecked) + { + DrawCheckmark(canvas, boxRect); + } + } + + private void DrawCheckmark(SKCanvas canvas, SKRect boxRect) + { + using var paint = new SKPaint + { + Color = CheckColor, + IsAntialias = true, + Style = SKPaintStyle.Stroke, + StrokeWidth = CheckStrokeWidth, + StrokeCap = SKStrokeCap.Round, + StrokeJoin = SKStrokeJoin.Round + }; + + // Checkmark path - a simple check + var padding = BoxSize * 0.2f; + var left = boxRect.Left + padding; + var right = boxRect.Right - padding; + var top = boxRect.Top + padding; + var bottom = boxRect.Bottom - padding; + + // Check starts from bottom-left, goes to middle-bottom, then to top-right + using var path = new SKPath(); + path.MoveTo(left, boxRect.MidY); + path.LineTo(boxRect.MidX - padding * 0.3f, bottom - padding * 0.5f); + path.LineTo(right, top + padding * 0.3f); + + canvas.DrawPath(path, paint); + } + + public override void OnPointerEntered(PointerEventArgs e) + { + if (!IsEnabled) return; + IsHovered = true; + SkiaVisualStateManager.GoToState(this, SkiaVisualStateManager.CommonStates.PointerOver); + Invalidate(); + } + + public override void OnPointerExited(PointerEventArgs e) + { + IsHovered = false; + SkiaVisualStateManager.GoToState(this, IsEnabled ? SkiaVisualStateManager.CommonStates.Normal : SkiaVisualStateManager.CommonStates.Disabled); + Invalidate(); + } + + public override void OnPointerPressed(PointerEventArgs e) + { + if (!IsEnabled) return; + IsChecked = !IsChecked; + e.Handled = true; + } + + public override void OnPointerReleased(PointerEventArgs e) + { + // Toggle handled in OnPointerPressed + } + + public override void OnKeyDown(KeyEventArgs e) + { + if (!IsEnabled) return; + + // Toggle on Space + if (e.Key == Key.Space) + { + IsChecked = !IsChecked; + e.Handled = true; + } + } + + protected override void OnEnabledChanged() + { + base.OnEnabledChanged(); + SkiaVisualStateManager.GoToState(this, IsEnabled ? SkiaVisualStateManager.CommonStates.Normal : SkiaVisualStateManager.CommonStates.Disabled); + } + + protected override SKSize MeasureOverride(SKSize availableSize) + { + // Add some padding around the box for touch targets + return new SKSize(BoxSize + 8, BoxSize + 8); + } +} + +/// +/// Event args for checked changed events. +/// +public class CheckedChangedEventArgs : EventArgs +{ + public bool IsChecked { get; } + + public CheckedChangedEventArgs(bool isChecked) + { + IsChecked = isChecked; + } } diff --git a/Views/SkiaCollectionView.cs b/Views/SkiaCollectionView.cs index f6e55c3..9860d79 100644 --- a/Views/SkiaCollectionView.cs +++ b/Views/SkiaCollectionView.cs @@ -1,988 +1,849 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.Maui.Controls; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using SkiaSharp; +using System.Collections; +using Microsoft.Maui.Graphics; namespace Microsoft.Maui.Platform; +/// +/// Selection mode for collection views. +/// +public enum SkiaSelectionMode +{ + None, + Single, + Multiple +} + +/// +/// Layout orientation for items. +/// +public enum ItemsLayoutOrientation +{ + Vertical, + Horizontal +} + +/// +/// Skia-rendered CollectionView with selection, headers, and flexible layouts. +/// public class SkiaCollectionView : SkiaItemsView { - public static readonly BindableProperty SelectionModeProperty = BindableProperty.Create("SelectionMode", typeof(SkiaSelectionMode), typeof(SkiaCollectionView), (object)SkiaSelectionMode.Single, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaCollectionView)(object)b).OnSelectionModeChanged(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + #region BindableProperties - public static readonly BindableProperty SelectedItemProperty = BindableProperty.Create("SelectedItem", typeof(object), typeof(SkiaCollectionView), (object)null, (BindingMode)1, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaCollectionView)(object)b).OnSelectedItemChanged(n); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for SelectionMode. + /// + public static readonly BindableProperty SelectionModeProperty = + BindableProperty.Create( + nameof(SelectionMode), + typeof(SkiaSelectionMode), + typeof(SkiaCollectionView), + SkiaSelectionMode.Single, + propertyChanged: (b, o, n) => ((SkiaCollectionView)b).OnSelectionModeChanged()); - public static readonly BindableProperty OrientationProperty = BindableProperty.Create("Orientation", typeof(ItemsLayoutOrientation), typeof(SkiaCollectionView), (object)ItemsLayoutOrientation.Vertical, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaCollectionView)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for SelectedItem. + /// + public static readonly BindableProperty SelectedItemProperty = + BindableProperty.Create( + nameof(SelectedItem), + typeof(object), + typeof(SkiaCollectionView), + null, + BindingMode.TwoWay, + propertyChanged: (b, o, n) => ((SkiaCollectionView)b).OnSelectedItemChanged(n)); - public static readonly BindableProperty SpanCountProperty = BindableProperty.Create("SpanCount", typeof(int), typeof(SkiaCollectionView), (object)1, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaCollectionView)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)((BindableObject b, object v) => Math.Max(1, (int)v)), (CreateDefaultValueDelegate)null); + /// + /// Bindable property for Orientation. + /// + public static readonly BindableProperty OrientationProperty = + BindableProperty.Create( + nameof(Orientation), + typeof(ItemsLayoutOrientation), + typeof(SkiaCollectionView), + ItemsLayoutOrientation.Vertical, + propertyChanged: (b, o, n) => ((SkiaCollectionView)b).Invalidate()); - public static readonly BindableProperty GridItemWidthProperty = BindableProperty.Create("GridItemWidth", typeof(float), typeof(SkiaCollectionView), (object)100f, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaCollectionView)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for SpanCount. + /// + public static readonly BindableProperty SpanCountProperty = + BindableProperty.Create( + nameof(SpanCount), + typeof(int), + typeof(SkiaCollectionView), + 1, + coerceValue: (b, v) => Math.Max(1, (int)v), + propertyChanged: (b, o, n) => ((SkiaCollectionView)b).Invalidate()); - public static readonly BindableProperty HeaderProperty = BindableProperty.Create("Header", typeof(object), typeof(SkiaCollectionView), (object)null, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaCollectionView)(object)b).OnHeaderChanged(n); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for GridItemWidth. + /// + public static readonly BindableProperty GridItemWidthProperty = + BindableProperty.Create( + nameof(GridItemWidth), + typeof(float), + typeof(SkiaCollectionView), + 100f, + propertyChanged: (b, o, n) => ((SkiaCollectionView)b).Invalidate()); - public static readonly BindableProperty FooterProperty = BindableProperty.Create("Footer", typeof(object), typeof(SkiaCollectionView), (object)null, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaCollectionView)(object)b).OnFooterChanged(n); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for Header. + /// + public static readonly BindableProperty HeaderProperty = + BindableProperty.Create( + nameof(Header), + typeof(object), + typeof(SkiaCollectionView), + null, + propertyChanged: (b, o, n) => ((SkiaCollectionView)b).OnHeaderChanged(n)); - public static readonly BindableProperty HeaderHeightProperty = BindableProperty.Create("HeaderHeight", typeof(float), typeof(SkiaCollectionView), (object)0f, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaCollectionView)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for Footer. + /// + public static readonly BindableProperty FooterProperty = + BindableProperty.Create( + nameof(Footer), + typeof(object), + typeof(SkiaCollectionView), + null, + propertyChanged: (b, o, n) => ((SkiaCollectionView)b).OnFooterChanged(n)); - public static readonly BindableProperty FooterHeightProperty = BindableProperty.Create("FooterHeight", typeof(float), typeof(SkiaCollectionView), (object)0f, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaCollectionView)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for HeaderHeight. + /// + public static readonly BindableProperty HeaderHeightProperty = + BindableProperty.Create( + nameof(HeaderHeight), + typeof(float), + typeof(SkiaCollectionView), + 0f, + propertyChanged: (b, o, n) => ((SkiaCollectionView)b).Invalidate()); - public static readonly BindableProperty SelectionColorProperty = BindableProperty.Create("SelectionColor", typeof(SKColor), typeof(SkiaCollectionView), (object)new SKColor((byte)33, (byte)150, (byte)243, (byte)89), (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaCollectionView)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for FooterHeight. + /// + public static readonly BindableProperty FooterHeightProperty = + BindableProperty.Create( + nameof(FooterHeight), + typeof(float), + typeof(SkiaCollectionView), + 0f, + propertyChanged: (b, o, n) => ((SkiaCollectionView)b).Invalidate()); - public static readonly BindableProperty HeaderBackgroundColorProperty = BindableProperty.Create("HeaderBackgroundColor", typeof(SKColor), typeof(SkiaCollectionView), (object)new SKColor((byte)245, (byte)245, (byte)245), (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaCollectionView)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for SelectionColor. + /// + public static readonly BindableProperty SelectionColorProperty = + BindableProperty.Create( + nameof(SelectionColor), + typeof(SKColor), + typeof(SkiaCollectionView), + new SKColor(0x21, 0x96, 0xF3, 0x59), + propertyChanged: (b, o, n) => ((SkiaCollectionView)b).Invalidate()); - public static readonly BindableProperty FooterBackgroundColorProperty = BindableProperty.Create("FooterBackgroundColor", typeof(SKColor), typeof(SkiaCollectionView), (object)new SKColor((byte)245, (byte)245, (byte)245), (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaCollectionView)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for HeaderBackgroundColor. + /// + public static readonly BindableProperty HeaderBackgroundColorProperty = + BindableProperty.Create( + nameof(HeaderBackgroundColor), + typeof(SKColor), + typeof(SkiaCollectionView), + new SKColor(0xF5, 0xF5, 0xF5), + propertyChanged: (b, o, n) => ((SkiaCollectionView)b).Invalidate()); - private List _selectedItems = new List(); + /// + /// Bindable property for FooterBackgroundColor. + /// + public static readonly BindableProperty FooterBackgroundColorProperty = + BindableProperty.Create( + nameof(FooterBackgroundColor), + typeof(SKColor), + typeof(SkiaCollectionView), + new SKColor(0xF5, 0xF5, 0xF5), + propertyChanged: (b, o, n) => ((SkiaCollectionView)b).Invalidate()); - private int _selectedIndex = -1; + #endregion - private bool _isSelectingItem; + private List _selectedItems = new(); + private int _selectedIndex = -1; - private bool _heightsChangedDuringDraw; + // Track if heights changed during draw (requires redraw for correct positioning) + private bool _heightsChangedDuringDraw; - public SkiaSelectionMode SelectionMode - { - get - { - return (SkiaSelectionMode)((BindableObject)this).GetValue(SelectionModeProperty); - } - set - { - ((BindableObject)this).SetValue(SelectionModeProperty, (object)value); - } - } + // Uses parent's _itemViewCache for virtualization - public object? SelectedItem - { - get - { - return ((BindableObject)this).GetValue(SelectedItemProperty); - } - set - { - ((BindableObject)this).SetValue(SelectedItemProperty, value); - } - } + protected override void RefreshItems() + { + // Clear selection when items change to avoid stale references + _selectedItems.Clear(); + SetValue(SelectedItemProperty, null); + _selectedIndex = -1; - public IList SelectedItems => _selectedItems.AsReadOnly(); + base.RefreshItems(); + } - public override int SelectedIndex - { - get - { - return _selectedIndex; - } - set - { - if (SelectionMode != SkiaSelectionMode.None) - { - object itemAt = GetItemAt(value); - if (itemAt != null) - { - SelectedItem = itemAt; - } - } - } - } + private void OnSelectionModeChanged() + { + var mode = SelectionMode; + if (mode == SkiaSelectionMode.None) + { + ClearSelection(); + } + else if (mode == SkiaSelectionMode.Single && _selectedItems.Count > 1) + { + // Keep only first selected + var first = _selectedItems.FirstOrDefault(); + ClearSelection(); + if (first != null) + { + SelectItem(first); + } + } + Invalidate(); + } - public ItemsLayoutOrientation Orientation - { - get - { - return (ItemsLayoutOrientation)((BindableObject)this).GetValue(OrientationProperty); - } - set - { - ((BindableObject)this).SetValue(OrientationProperty, (object)value); - } - } + private void OnSelectedItemChanged(object? newValue) + { + if (SelectionMode == SkiaSelectionMode.None) return; - public int SpanCount - { - get - { - return (int)((BindableObject)this).GetValue(SpanCountProperty); - } - set - { - ((BindableObject)this).SetValue(SpanCountProperty, (object)value); - } - } + ClearSelection(); + if (newValue != null) + { + SelectItem(newValue); + } + } - public float GridItemWidth - { - get - { - return (float)((BindableObject)this).GetValue(GridItemWidthProperty); - } - set - { - ((BindableObject)this).SetValue(GridItemWidthProperty, (object)value); - } - } + private void OnHeaderChanged(object? newValue) + { + HeaderHeight = newValue != null ? 44 : 0; + Invalidate(); + } - public object? Header - { - get - { - return ((BindableObject)this).GetValue(HeaderProperty); - } - set - { - ((BindableObject)this).SetValue(HeaderProperty, value); - } - } + private void OnFooterChanged(object? newValue) + { + FooterHeight = newValue != null ? 44 : 0; + Invalidate(); + } - public object? Footer - { - get - { - return ((BindableObject)this).GetValue(FooterProperty); - } - set - { - ((BindableObject)this).SetValue(FooterProperty, value); - } - } + public SkiaSelectionMode SelectionMode + { + get => (SkiaSelectionMode)GetValue(SelectionModeProperty); + set => SetValue(SelectionModeProperty, value); + } - public float HeaderHeight - { - get - { - return (float)((BindableObject)this).GetValue(HeaderHeightProperty); - } - set - { - ((BindableObject)this).SetValue(HeaderHeightProperty, (object)value); - } - } + public object? SelectedItem + { + get => GetValue(SelectedItemProperty); + set => SetValue(SelectedItemProperty, value); + } - public float FooterHeight - { - get - { - return (float)((BindableObject)this).GetValue(FooterHeightProperty); - } - set - { - ((BindableObject)this).SetValue(FooterHeightProperty, (object)value); - } - } + public IList SelectedItems => _selectedItems.AsReadOnly(); - public SKColor SelectionColor - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKColor)((BindableObject)this).GetValue(SelectionColorProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(SelectionColorProperty, (object)value); - } - } + public override int SelectedIndex + { + get => _selectedIndex; + set + { + if (SelectionMode == SkiaSelectionMode.None) return; - public SKColor HeaderBackgroundColor - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKColor)((BindableObject)this).GetValue(HeaderBackgroundColorProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(HeaderBackgroundColorProperty, (object)value); - } - } + var item = GetItemAt(value); + if (item != null) + { + SelectedItem = item; + } + } + } - public SKColor FooterBackgroundColor - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKColor)((BindableObject)this).GetValue(FooterBackgroundColorProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(FooterBackgroundColorProperty, (object)value); - } - } + public ItemsLayoutOrientation Orientation + { + get => (ItemsLayoutOrientation)GetValue(OrientationProperty); + set => SetValue(OrientationProperty, value); + } - public event EventHandler? SelectionChanged; + public int SpanCount + { + get => (int)GetValue(SpanCountProperty); + set => SetValue(SpanCountProperty, value); + } - protected override void RefreshItems() - { - _selectedItems.Clear(); - ((BindableObject)this).SetValue(SelectedItemProperty, (object)null); - _selectedIndex = -1; - base.RefreshItems(); - } + public float GridItemWidth + { + get => (float)GetValue(GridItemWidthProperty); + set => SetValue(GridItemWidthProperty, value); + } - private void OnSelectionModeChanged() - { - switch (SelectionMode) - { - case SkiaSelectionMode.None: - ClearSelection(); - break; - case SkiaSelectionMode.Single: - if (_selectedItems.Count > 1) - { - object obj = _selectedItems.FirstOrDefault(); - ClearSelection(); - if (obj != null) - { - SelectItem(obj); - } - } - break; - } - Invalidate(); - } + public object? Header + { + get => GetValue(HeaderProperty); + set => SetValue(HeaderProperty, value); + } - private void OnSelectedItemChanged(object? newValue) - { - if (SelectionMode != SkiaSelectionMode.None && !_isSelectingItem) - { - ClearSelection(); - if (newValue != null) - { - SelectItem(newValue); - } - } - } + public object? Footer + { + get => GetValue(FooterProperty); + set => SetValue(FooterProperty, value); + } - private void OnHeaderChanged(object? newValue) - { - HeaderHeight = ((newValue != null) ? 44 : 0); - Invalidate(); - } + public float HeaderHeight + { + get => (float)GetValue(HeaderHeightProperty); + set => SetValue(HeaderHeightProperty, value); + } - private void OnFooterChanged(object? newValue) - { - FooterHeight = ((newValue != null) ? 44 : 0); - Invalidate(); - } + public float FooterHeight + { + get => (float)GetValue(FooterHeightProperty); + set => SetValue(FooterHeightProperty, value); + } - private void SelectItem(object item) - { - if (SelectionMode == SkiaSelectionMode.None) - { - return; - } - List previousSelection = _selectedItems.ToList(); - if (SelectionMode == SkiaSelectionMode.Single) - { - _selectedItems.Clear(); - _selectedItems.Add(item); - ((BindableObject)this).SetValue(SelectedItemProperty, item); - for (int i = 0; i < base.ItemCount; i++) - { - if (GetItemAt(i) == item) - { - _selectedIndex = i; - break; - } - } - } - else - { - if (_selectedItems.Contains(item)) - { - _selectedItems.Remove(item); - if (SelectedItem == item) - { - ((BindableObject)this).SetValue(SelectedItemProperty, _selectedItems.FirstOrDefault()); - } - } - else - { - _selectedItems.Add(item); - ((BindableObject)this).SetValue(SelectedItemProperty, item); - } - _selectedIndex = ((SelectedItem != null) ? GetIndexOf(SelectedItem) : (-1)); - } - this.SelectionChanged?.Invoke(this, new CollectionSelectionChangedEventArgs(previousSelection, _selectedItems.ToList())); - Invalidate(); - } + public SKColor SelectionColor + { + get => (SKColor)GetValue(SelectionColorProperty); + set => SetValue(SelectionColorProperty, value); + } - private int GetIndexOf(object item) - { - for (int i = 0; i < base.ItemCount; i++) - { - if (GetItemAt(i) == item) - { - return i; - } - } - return -1; - } + public SKColor HeaderBackgroundColor + { + get => (SKColor)GetValue(HeaderBackgroundColorProperty); + set => SetValue(HeaderBackgroundColorProperty, value); + } - private void ClearSelection() - { - List list = _selectedItems.ToList(); - _selectedItems.Clear(); - ((BindableObject)this).SetValue(SelectedItemProperty, (object)null); - _selectedIndex = -1; - if (list.Count > 0) - { - this.SelectionChanged?.Invoke(this, new CollectionSelectionChangedEventArgs(list, new List())); - } - } + public SKColor FooterBackgroundColor + { + get => (SKColor)GetValue(FooterBackgroundColorProperty); + set => SetValue(FooterBackgroundColorProperty, value); + } - protected override void OnItemTapped(int index, object item) - { - if (_isSelectingItem) - { - return; - } - _isSelectingItem = true; - try - { - if (SelectionMode != SkiaSelectionMode.None) - { - SelectItem(item); - } - base.OnItemTapped(index, item); - } - finally - { - _isSelectingItem = false; - } - } + public event EventHandler? SelectionChanged; - protected override void DrawItem(SKCanvas canvas, object item, int index, SKRect bounds, SKPaint paint) - { - //IL_002f: Unknown result type (might be due to invalid IL or missing references) - //IL_0245: Unknown result type (might be due to invalid IL or missing references) - //IL_026b: Unknown result type (might be due to invalid IL or missing references) - //IL_0271: Expected O, but got Unknown - //IL_0236: Unknown result type (might be due to invalid IL or missing references) - //IL_0272: Unknown result type (might be due to invalid IL or missing references) - //IL_0277: Unknown result type (might be due to invalid IL or missing references) - //IL_0278: Unknown result type (might be due to invalid IL or missing references) - //IL_0282: Unknown result type (might be due to invalid IL or missing references) - //IL_028a: Expected O, but got Unknown - //IL_00d4: Unknown result type (might be due to invalid IL or missing references) - //IL_00d6: Unknown result type (might be due to invalid IL or missing references) - //IL_00db: Unknown result type (might be due to invalid IL or missing references) - //IL_02a2: Unknown result type (might be due to invalid IL or missing references) - //IL_0176: Unknown result type (might be due to invalid IL or missing references) - //IL_0320: Unknown result type (might be due to invalid IL or missing references) - //IL_018b: Unknown result type (might be due to invalid IL or missing references) - //IL_019e: Unknown result type (might be due to invalid IL or missing references) - //IL_01f3: Unknown result type (might be due to invalid IL or missing references) - bool flag = _selectedItems.Contains(item); - if (Orientation == ItemsLayoutOrientation.Vertical && SpanCount == 1) - { - paint.Color = new SKColor((byte)224, (byte)224, (byte)224); - paint.Style = (SKPaintStyle)1; - paint.StrokeWidth = 1f; - canvas.DrawLine(((SKRect)(ref bounds)).Left, ((SKRect)(ref bounds)).Bottom, ((SKRect)(ref bounds)).Right, ((SKRect)(ref bounds)).Bottom, paint); - } - if (base.ItemViewCreator != null) - { - if (!_itemViewCache.TryGetValue(index, out SkiaView value) || value == null) - { - value = base.ItemViewCreator(item); - if (value != null) - { - value.Parent = this; - _itemViewCache[index] = value; - } - } - if (value != null) - { - try - { - SKSize availableSize = default(SKSize); - ((SKSize)(ref availableSize))._002Ector(((SKRect)(ref bounds)).Width, float.MaxValue); - SKSize val = value.Measure(availableSize); - float num = ((SKSize)(ref val)).Height; - if (float.IsNaN(num) || float.IsInfinity(num) || num > 10000f) - { - num = base.ItemHeight; - } - float num2 = Math.Max(num, base.ItemHeight); - if (!_itemHeights.TryGetValue(index, out var value2) || Math.Abs(value2 - num2) > 1f) - { - _itemHeights[index] = num2; - _heightsChangedDuringDraw = true; - } - SKRect val2 = default(SKRect); - ((SKRect)(ref val2))._002Ector(((SKRect)(ref bounds)).Left, ((SKRect)(ref bounds)).Top, ((SKRect)(ref bounds)).Right, ((SKRect)(ref bounds)).Top + num2); - value.Arrange(val2); - value.Draw(canvas); - if (flag) - { - paint.Color = SelectionColor; - paint.Style = (SKPaintStyle)0; - canvas.DrawRoundRect(val2, 12f, 12f, paint); - } - if (flag && SelectionMode == SkiaSelectionMode.Multiple) - { - DrawCheckmark(canvas, new SKRect(((SKRect)(ref val2)).Right - 32f, ((SKRect)(ref val2)).MidY - 8f, ((SKRect)(ref val2)).Right - 16f, ((SKRect)(ref val2)).MidY + 8f)); - } - return; - } - catch (Exception ex) - { - Console.WriteLine("[SkiaCollectionView.DrawItem] EXCEPTION: " + ex.Message + "\n" + ex.StackTrace); - return; - } - } - } - if (base.ItemRenderer != null && base.ItemRenderer(item, index, bounds, canvas, paint)) - { - return; - } - paint.Color = SKColors.Black; - paint.Style = (SKPaintStyle)0; - SKFont val3 = new SKFont(SKTypeface.Default, 14f, 1f, 0f); - try - { - SKPaint val4 = new SKPaint(val3) - { - Color = SKColors.Black, - IsAntialias = true - }; - try - { - string text = item?.ToString() ?? ""; - SKRect val5 = default(SKRect); - val4.MeasureText(text, ref val5); - float num3 = ((SKRect)(ref bounds)).Left + 16f; - float num4 = ((SKRect)(ref bounds)).MidY - ((SKRect)(ref val5)).MidY; - canvas.DrawText(text, num3, num4, val4); - if (flag && SelectionMode == SkiaSelectionMode.Multiple) - { - DrawCheckmark(canvas, new SKRect(((SKRect)(ref bounds)).Right - 32f, ((SKRect)(ref bounds)).MidY - 8f, ((SKRect)(ref bounds)).Right - 16f, ((SKRect)(ref bounds)).MidY + 8f)); - } - } - finally - { - ((IDisposable)val4)?.Dispose(); - } - } - finally - { - ((IDisposable)val3)?.Dispose(); - } - } + private void SelectItem(object item) + { + if (SelectionMode == SkiaSelectionMode.None) return; - private void DrawCheckmark(SKCanvas canvas, SKRect bounds) - { - //IL_0000: Unknown result type (might be due to invalid IL or missing references) - //IL_0005: Unknown result type (might be due to invalid IL or missing references) - //IL_0012: Unknown result type (might be due to invalid IL or missing references) - //IL_001c: Unknown result type (might be due to invalid IL or missing references) - //IL_0023: Unknown result type (might be due to invalid IL or missing references) - //IL_002e: Unknown result type (might be due to invalid IL or missing references) - //IL_0035: Unknown result type (might be due to invalid IL or missing references) - //IL_003d: Expected O, but got Unknown - //IL_003d: Unknown result type (might be due to invalid IL or missing references) - //IL_0043: Expected O, but got Unknown - SKPaint val = new SKPaint - { - Color = new SKColor((byte)33, (byte)150, (byte)243), - Style = (SKPaintStyle)1, - StrokeWidth = 2f, - IsAntialias = true, - StrokeCap = (SKStrokeCap)1 - }; - try - { - SKPath val2 = new SKPath(); - try - { - val2.MoveTo(((SKRect)(ref bounds)).Left, ((SKRect)(ref bounds)).MidY); - val2.LineTo(((SKRect)(ref bounds)).MidX - 2f, ((SKRect)(ref bounds)).Bottom - 2f); - val2.LineTo(((SKRect)(ref bounds)).Right, ((SKRect)(ref bounds)).Top + 2f); - canvas.DrawPath(val2, val); - } - finally - { - ((IDisposable)val2)?.Dispose(); - } - } - finally - { - ((IDisposable)val)?.Dispose(); - } - } + var oldSelectedItems = _selectedItems.ToList(); - protected override void OnDraw(SKCanvas canvas, SKRect bounds) - { - //IL_0008: Unknown result type (might be due to invalid IL or missing references) - //IL_000d: Unknown result type (might be due to invalid IL or missing references) - //IL_0019: Unknown result type (might be due to invalid IL or missing references) - //IL_001e: Unknown result type (might be due to invalid IL or missing references) - //IL_0020: Unknown result type (might be due to invalid IL or missing references) - //IL_002a: Unknown result type (might be due to invalid IL or missing references) - //IL_0032: Expected O, but got Unknown - //IL_0033: Unknown result type (might be due to invalid IL or missing references) - //IL_0087: Unknown result type (might be due to invalid IL or missing references) - //IL_010f: Unknown result type (might be due to invalid IL or missing references) - //IL_00ce: Unknown result type (might be due to invalid IL or missing references) - //IL_012b: Unknown result type (might be due to invalid IL or missing references) - //IL_0121: Unknown result type (might be due to invalid IL or missing references) - _heightsChangedDuringDraw = false; - if (base.BackgroundColor != SKColors.Transparent) - { - SKPaint val = new SKPaint - { - Color = base.BackgroundColor, - Style = (SKPaintStyle)0 - }; - try - { - canvas.DrawRect(bounds, val); - } - finally - { - ((IDisposable)val)?.Dispose(); - } - } - if (Header != null && HeaderHeight > 0f) - { - SKRect bounds2 = default(SKRect); - ((SKRect)(ref bounds2))._002Ector(((SKRect)(ref bounds)).Left, ((SKRect)(ref bounds)).Top, ((SKRect)(ref bounds)).Right, ((SKRect)(ref bounds)).Top + HeaderHeight); - DrawHeader(canvas, bounds2); - } - if (Footer != null && FooterHeight > 0f) - { - SKRect bounds3 = default(SKRect); - ((SKRect)(ref bounds3))._002Ector(((SKRect)(ref bounds)).Left, ((SKRect)(ref bounds)).Bottom - FooterHeight, ((SKRect)(ref bounds)).Right, ((SKRect)(ref bounds)).Bottom); - DrawFooter(canvas, bounds3); - } - SKRect bounds4 = default(SKRect); - ((SKRect)(ref bounds4))._002Ector(((SKRect)(ref bounds)).Left, ((SKRect)(ref bounds)).Top + HeaderHeight, ((SKRect)(ref bounds)).Right, ((SKRect)(ref bounds)).Bottom - FooterHeight); - if (base.ItemCount == 0) - { - DrawEmptyView(canvas, bounds4); - return; - } - if (SpanCount > 1) - { - DrawGridItems(canvas, bounds4); - } - else - { - DrawListItems(canvas, bounds4); - } - if (_heightsChangedDuringDraw) - { - _heightsChangedDuringDraw = false; - Invalidate(); - } - } + if (SelectionMode == SkiaSelectionMode.Single) + { + _selectedItems.Clear(); + _selectedItems.Add(item); + SetValue(SelectedItemProperty, item); - private void DrawListItems(SKCanvas canvas, SKRect bounds) - { - //IL_0008: Unknown result type (might be due to invalid IL or missing references) - //IL_0010: Unknown result type (might be due to invalid IL or missing references) - //IL_0015: Unknown result type (might be due to invalid IL or missing references) - //IL_001d: Expected O, but got Unknown - //IL_011f: Unknown result type (might be due to invalid IL or missing references) - //IL_00db: Unknown result type (might be due to invalid IL or missing references) - canvas.Save(); - canvas.ClipRect(bounds, (SKClipOperation)1, false); - SKPaint val = new SKPaint - { - IsAntialias = true - }; - try - { - float scrollOffset = GetScrollOffset(); - int num = 0; - float num2 = 0f; - for (int i = 0; i < base.ItemCount; i++) - { - float itemHeight = GetItemHeight(i); - if (num2 + itemHeight > scrollOffset) - { - num = i; - break; - } - num2 += itemHeight + base.ItemSpacing; - } - float num3 = ((SKRect)(ref bounds)).Top + GetItemOffset(num) - scrollOffset; - SKRect bounds2 = default(SKRect); - for (int j = num; j < base.ItemCount; j++) - { - float itemHeight2 = GetItemHeight(j); - ((SKRect)(ref bounds2))._002Ector(((SKRect)(ref bounds)).Left, num3, ((SKRect)(ref bounds)).Right - 8f, num3 + itemHeight2); - if (((SKRect)(ref bounds2)).Top > ((SKRect)(ref bounds)).Bottom) - { - break; - } - if (((SKRect)(ref bounds2)).Bottom >= ((SKRect)(ref bounds)).Top) - { - object itemAt = GetItemAt(j); - if (itemAt != null) - { - DrawItem(canvas, itemAt, j, bounds2, val); - } - } - num3 += itemHeight2 + base.ItemSpacing; - } - canvas.Restore(); - float totalContentHeight = base.TotalContentHeight; - if (totalContentHeight > ((SKRect)(ref bounds)).Height) - { - DrawScrollBarInternal(canvas, bounds, scrollOffset, totalContentHeight); - } - } - finally - { - ((IDisposable)val)?.Dispose(); - } - } + // Find index + for (int i = 0; i < ItemCount; i++) + { + if (GetItemAt(i) == item) + { + _selectedIndex = i; + break; + } + } + } + else // Multiple + { + if (_selectedItems.Contains(item)) + { + _selectedItems.Remove(item); + if (SelectedItem == item) + { + SetValue(SelectedItemProperty, _selectedItems.FirstOrDefault()); + } + } + else + { + _selectedItems.Add(item); + SetValue(SelectedItemProperty, item); + } - private void DrawGridItems(SKCanvas canvas, SKRect bounds) - { - //IL_0008: Unknown result type (might be due to invalid IL or missing references) - //IL_0010: Unknown result type (might be due to invalid IL or missing references) - //IL_0015: Unknown result type (might be due to invalid IL or missing references) - //IL_001d: Expected O, but got Unknown - //IL_01e4: Unknown result type (might be due to invalid IL or missing references) - //IL_0140: Unknown result type (might be due to invalid IL or missing references) - //IL_0145: Unknown result type (might be due to invalid IL or missing references) - //IL_016c: Unknown result type (might be due to invalid IL or missing references) - //IL_0164: Unknown result type (might be due to invalid IL or missing references) - //IL_0176: Unknown result type (might be due to invalid IL or missing references) - //IL_017f: Expected O, but got Unknown - //IL_0180: Unknown result type (might be due to invalid IL or missing references) - //IL_0187: Unknown result type (might be due to invalid IL or missing references) - //IL_0193: Expected O, but got Unknown - //IL_0199: Unknown result type (might be due to invalid IL or missing references) - canvas.Save(); - canvas.ClipRect(bounds, (SKClipOperation)1, false); - SKPaint val = new SKPaint - { - IsAntialias = true - }; - try - { - float num = (((SKRect)(ref bounds)).Width - 8f) / (float)SpanCount; - float itemHeight = base.ItemHeight; - int num2 = (int)Math.Ceiling((double)base.ItemCount / (double)SpanCount); - float num3 = (float)num2 * (itemHeight + base.ItemSpacing) - base.ItemSpacing; - float scrollOffset = GetScrollOffset(); - int num4 = Math.Max(0, (int)(scrollOffset / (itemHeight + base.ItemSpacing))); - int num5 = Math.Min(num2 - 1, (int)((scrollOffset + ((SKRect)(ref bounds)).Height) / (itemHeight + base.ItemSpacing)) + 1); - SKRect val2 = default(SKRect); - for (int i = num4; i <= num5; i++) - { - float num6 = ((SKRect)(ref bounds)).Top + (float)i * (itemHeight + base.ItemSpacing) - scrollOffset; - for (int j = 0; j < SpanCount; j++) - { - int num7 = i * SpanCount + j; - if (num7 >= base.ItemCount) - { - break; - } - float num8 = ((SKRect)(ref bounds)).Left + (float)j * num; - ((SKRect)(ref val2))._002Ector(num8 + 2f, num6, num8 + num - 2f, num6 + itemHeight); - if (((SKRect)(ref val2)).Bottom < ((SKRect)(ref bounds)).Top || ((SKRect)(ref val2)).Top > ((SKRect)(ref bounds)).Bottom) - { - continue; - } - object itemAt = GetItemAt(num7); - if (itemAt != null) - { - SKPaint val3 = new SKPaint - { - Color = (SKColor)(_selectedItems.Contains(itemAt) ? SelectionColor : new SKColor((byte)250, (byte)250, (byte)250)), - Style = (SKPaintStyle)0 - }; - try - { - canvas.DrawRoundRect(new SKRoundRect(val2, 4f), val3); - DrawItem(canvas, itemAt, num7, val2, val); - } - finally - { - ((IDisposable)val3)?.Dispose(); - } - } - } - } - canvas.Restore(); - if (num3 > ((SKRect)(ref bounds)).Height) - { - DrawScrollBarInternal(canvas, bounds, scrollOffset, num3); - } - } - finally - { - ((IDisposable)val)?.Dispose(); - } - } + _selectedIndex = SelectedItem != null ? GetIndexOf(SelectedItem) : -1; + } - private void DrawScrollBarInternal(SKCanvas canvas, SKRect bounds, float scrollOffset, float totalHeight) - { - //IL_0039: Unknown result type (might be due to invalid IL or missing references) - //IL_003e: Unknown result type (might be due to invalid IL or missing references) - //IL_0044: Unknown result type (might be due to invalid IL or missing references) - //IL_004e: Unknown result type (might be due to invalid IL or missing references) - //IL_0056: Expected O, but got Unknown - //IL_0057: Unknown result type (might be due to invalid IL or missing references) - //IL_005d: Unknown result type (might be due to invalid IL or missing references) - //IL_0068: Expected O, but got Unknown - //IL_00e1: Unknown result type (might be due to invalid IL or missing references) - //IL_00e6: Unknown result type (might be due to invalid IL or missing references) - //IL_00e8: Unknown result type (might be due to invalid IL or missing references) - //IL_00ed: Unknown result type (might be due to invalid IL or missing references) - //IL_00f9: Unknown result type (might be due to invalid IL or missing references) - //IL_0103: Unknown result type (might be due to invalid IL or missing references) - //IL_010a: Unknown result type (might be due to invalid IL or missing references) - //IL_0113: Expected O, but got Unknown - //IL_0114: Unknown result type (might be due to invalid IL or missing references) - //IL_011b: Unknown result type (might be due to invalid IL or missing references) - //IL_0127: Expected O, but got Unknown - float num = 6f; - float num2 = 2f; - SKRect val = default(SKRect); - ((SKRect)(ref val))._002Ector(((SKRect)(ref bounds)).Right - num - num2, ((SKRect)(ref bounds)).Top + num2, ((SKRect)(ref bounds)).Right - num2, ((SKRect)(ref bounds)).Bottom - num2); - SKPaint val2 = new SKPaint - { - Color = new SKColor((byte)0, (byte)0, (byte)0, (byte)20), - Style = (SKPaintStyle)0 - }; - try - { - canvas.DrawRoundRect(new SKRoundRect(val, 3f), val2); - float num3 = Math.Max(0f, totalHeight - ((SKRect)(ref bounds)).Height); - float num4 = ((SKRect)(ref bounds)).Height / totalHeight; - float height = ((SKRect)(ref val)).Height; - float num5 = Math.Max(30f, height * num4); - float num6 = ((num3 > 0f) ? (scrollOffset / num3) : 0f); - float num7 = ((SKRect)(ref val)).Top + (height - num5) * num6; - SKRect val3 = new SKRect(((SKRect)(ref val)).Left, num7, ((SKRect)(ref val)).Right, num7 + num5); - SKPaint val4 = new SKPaint - { - Color = new SKColor((byte)100, (byte)100, (byte)100, (byte)180), - Style = (SKPaintStyle)0, - IsAntialias = true - }; - try - { - canvas.DrawRoundRect(new SKRoundRect(val3, 3f), val4); - } - finally - { - ((IDisposable)val4)?.Dispose(); - } - } - finally - { - ((IDisposable)val2)?.Dispose(); - } - } + SelectionChanged?.Invoke(this, new CollectionSelectionChangedEventArgs(oldSelectedItems, _selectedItems.ToList())); + Invalidate(); + } - private float GetScrollOffset() - { - return _scrollOffset; - } + private int GetIndexOf(object item) + { + for (int i = 0; i < ItemCount; i++) + { + if (GetItemAt(i) == item) + return i; + } + return -1; + } - private void DrawHeader(SKCanvas canvas, SKRect bounds) - { - //IL_0000: Unknown result type (might be due to invalid IL or missing references) - //IL_0005: Unknown result type (might be due to invalid IL or missing references) - //IL_0007: Unknown result type (might be due to invalid IL or missing references) - //IL_0011: Unknown result type (might be due to invalid IL or missing references) - //IL_0019: Expected O, but got Unknown - //IL_001a: Unknown result type (might be due to invalid IL or missing references) - //IL_00cd: Unknown result type (might be due to invalid IL or missing references) - //IL_00d2: Unknown result type (might be due to invalid IL or missing references) - //IL_00e2: Unknown result type (might be due to invalid IL or missing references) - //IL_00ec: Unknown result type (might be due to invalid IL or missing references) - //IL_00f3: Unknown result type (might be due to invalid IL or missing references) - //IL_00ff: Expected O, but got Unknown - //IL_0055: Unknown result type (might be due to invalid IL or missing references) - //IL_005b: Expected O, but got Unknown - //IL_005c: Unknown result type (might be due to invalid IL or missing references) - //IL_0061: Unknown result type (might be due to invalid IL or missing references) - //IL_0062: Unknown result type (might be due to invalid IL or missing references) - //IL_006c: Unknown result type (might be due to invalid IL or missing references) - //IL_0075: Expected O, but got Unknown - //IL_0077: Unknown result type (might be due to invalid IL or missing references) - SKPaint val = new SKPaint - { - Color = HeaderBackgroundColor, - Style = (SKPaintStyle)0 - }; - try - { - canvas.DrawRect(bounds, val); - string text = Header.ToString() ?? ""; - if (!string.IsNullOrEmpty(text)) - { - SKFont val2 = new SKFont(SKTypeface.Default, 16f, 1f, 0f); - try - { - SKPaint val3 = new SKPaint(val2) - { - Color = SKColors.Black, - IsAntialias = true - }; - try - { - SKRect val4 = default(SKRect); - val3.MeasureText(text, ref val4); - float num = ((SKRect)(ref bounds)).Left + 16f; - float num2 = ((SKRect)(ref bounds)).MidY - ((SKRect)(ref val4)).MidY; - canvas.DrawText(text, num, num2, val3); - } - finally - { - ((IDisposable)val3)?.Dispose(); - } - } - finally - { - ((IDisposable)val2)?.Dispose(); - } - } - SKPaint val5 = new SKPaint - { - Color = new SKColor((byte)224, (byte)224, (byte)224), - Style = (SKPaintStyle)1, - StrokeWidth = 1f - }; - try - { - canvas.DrawLine(((SKRect)(ref bounds)).Left, ((SKRect)(ref bounds)).Bottom, ((SKRect)(ref bounds)).Right, ((SKRect)(ref bounds)).Bottom, val5); - } - finally - { - ((IDisposable)val5)?.Dispose(); - } - } - finally - { - ((IDisposable)val)?.Dispose(); - } - } + private void ClearSelection() + { + var oldItems = _selectedItems.ToList(); + _selectedItems.Clear(); + SetValue(SelectedItemProperty, null); + _selectedIndex = -1; - private void DrawFooter(SKCanvas canvas, SKRect bounds) - { - //IL_0000: Unknown result type (might be due to invalid IL or missing references) - //IL_0005: Unknown result type (might be due to invalid IL or missing references) - //IL_0007: Unknown result type (might be due to invalid IL or missing references) - //IL_0011: Unknown result type (might be due to invalid IL or missing references) - //IL_0019: Expected O, but got Unknown - //IL_001a: Unknown result type (might be due to invalid IL or missing references) - //IL_0021: Unknown result type (might be due to invalid IL or missing references) - //IL_0026: Unknown result type (might be due to invalid IL or missing references) - //IL_0036: Unknown result type (might be due to invalid IL or missing references) - //IL_0040: Unknown result type (might be due to invalid IL or missing references) - //IL_0047: Unknown result type (might be due to invalid IL or missing references) - //IL_0053: Expected O, but got Unknown - //IL_00aa: Unknown result type (might be due to invalid IL or missing references) - //IL_00b0: Expected O, but got Unknown - //IL_00b1: Unknown result type (might be due to invalid IL or missing references) - //IL_00b6: Unknown result type (might be due to invalid IL or missing references) - //IL_00c6: Unknown result type (might be due to invalid IL or missing references) - //IL_00d0: Unknown result type (might be due to invalid IL or missing references) - //IL_00d9: Expected O, but got Unknown - //IL_00db: Unknown result type (might be due to invalid IL or missing references) - SKPaint val = new SKPaint - { - Color = FooterBackgroundColor, - Style = (SKPaintStyle)0 - }; - try - { - canvas.DrawRect(bounds, val); - SKPaint val2 = new SKPaint - { - Color = new SKColor((byte)224, (byte)224, (byte)224), - Style = (SKPaintStyle)1, - StrokeWidth = 1f - }; - try - { - canvas.DrawLine(((SKRect)(ref bounds)).Left, ((SKRect)(ref bounds)).Top, ((SKRect)(ref bounds)).Right, ((SKRect)(ref bounds)).Top, val2); - string text = Footer.ToString() ?? ""; - if (string.IsNullOrEmpty(text)) - { - return; - } - SKFont val3 = new SKFont(SKTypeface.Default, 14f, 1f, 0f); - try - { - SKPaint val4 = new SKPaint(val3) - { - Color = new SKColor((byte)128, (byte)128, (byte)128), - IsAntialias = true - }; - try - { - SKRect val5 = default(SKRect); - val4.MeasureText(text, ref val5); - float num = ((SKRect)(ref bounds)).MidX - ((SKRect)(ref val5)).MidX; - float num2 = ((SKRect)(ref bounds)).MidY - ((SKRect)(ref val5)).MidY; - canvas.DrawText(text, num, num2, val4); - } - finally - { - ((IDisposable)val4)?.Dispose(); - } - } - finally - { - ((IDisposable)val3)?.Dispose(); - } - } - finally - { - ((IDisposable)val2)?.Dispose(); - } - } - finally - { - ((IDisposable)val)?.Dispose(); - } - } + if (oldItems.Count > 0) + { + SelectionChanged?.Invoke(this, new CollectionSelectionChangedEventArgs(oldItems, new List())); + } + } + + protected override void OnItemTapped(int index, object item) + { + if (SelectionMode != SkiaSelectionMode.None) + { + SelectItem(item); + } + + base.OnItemTapped(index, item); + } + + protected override void DrawItem(SKCanvas canvas, object item, int index, SKRect bounds, SKPaint paint) + { + bool isSelected = _selectedItems.Contains(item); + + // Draw separator (only for vertical list layout) + if (Orientation == ItemsLayoutOrientation.Vertical && SpanCount == 1) + { + paint.Color = new SKColor(0xE0, 0xE0, 0xE0); + paint.Style = SKPaintStyle.Stroke; + paint.StrokeWidth = 1; + canvas.DrawLine(bounds.Left, bounds.Bottom, bounds.Right, bounds.Bottom, paint); + } + + // Try to use ItemViewCreator for templated rendering (from DataTemplate) + if (ItemViewCreator != null) + { + // Get or create cached view for this index + if (!_itemViewCache.TryGetValue(index, out var itemView) || itemView == null) + { + itemView = ItemViewCreator(item); + if (itemView != null) + { + itemView.Parent = this; + _itemViewCache[index] = itemView; + } + } + + if (itemView != null) + { + try + { + // Measure with large height to get natural size + var availableSize = new SKSize(bounds.Width, float.MaxValue); + var measuredSize = itemView.Measure(availableSize); + + // Cap measured height - if item returns infinity/MaxValue, use ItemHeight as default + // This happens with Star-sized Grids that have no natural height preference + var rawHeight = measuredSize.Height; + if (float.IsNaN(rawHeight) || float.IsInfinity(rawHeight) || rawHeight > 10000) + { + rawHeight = ItemHeight; + } + // Ensure minimum height + var measuredHeight = Math.Max(rawHeight, ItemHeight); + if (!_itemHeights.TryGetValue(index, out var cachedHeight) || Math.Abs(cachedHeight - measuredHeight) > 1) + { + _itemHeights[index] = measuredHeight; + _heightsChangedDuringDraw = true; // Flag for redraw with correct positions + } + + // Arrange with the actual measured height + var actualBounds = new SKRect(bounds.Left, bounds.Top, bounds.Right, bounds.Top + measuredHeight); + itemView.Arrange(actualBounds); + itemView.Draw(canvas); + + // Draw selection highlight ON TOP of the item content (semi-transparent overlay) + if (isSelected) + { + paint.Color = SelectionColor; + paint.Style = SKPaintStyle.Fill; + canvas.DrawRoundRect(actualBounds, 12, 12, paint); + } + + // Draw checkmark for selected items in multiple selection mode + if (isSelected && SelectionMode == SkiaSelectionMode.Multiple) + { + DrawCheckmark(canvas, new SKRect(actualBounds.Right - 32, actualBounds.MidY - 8, actualBounds.Right - 16, actualBounds.MidY + 8)); + } + } + catch (Exception ex) + { + Console.WriteLine($"[SkiaCollectionView.DrawItem] EXCEPTION: {ex.Message}\n{ex.StackTrace}"); + } + return; + } + } + + // Use custom renderer if provided + if (ItemRenderer != null) + { + if (ItemRenderer(item, index, bounds, canvas, paint)) + return; + } + + // Default rendering - fall back to ToString + paint.Color = SKColors.Black; + paint.Style = SKPaintStyle.Fill; + + using var font = new SKFont(SKTypeface.Default, 14); + using var textPaint = new SKPaint(font) + { + Color = SKColors.Black, + IsAntialias = true + }; + + var text = item?.ToString() ?? ""; + var textBounds = new SKRect(); + textPaint.MeasureText(text, ref textBounds); + + var x = bounds.Left + 16; + var y = bounds.MidY - textBounds.MidY; + canvas.DrawText(text, x, y, textPaint); + + // Draw checkmark for selected items in multiple selection mode + if (isSelected && SelectionMode == SkiaSelectionMode.Multiple) + { + DrawCheckmark(canvas, new SKRect(bounds.Right - 32, bounds.MidY - 8, bounds.Right - 16, bounds.MidY + 8)); + } + } + + private void DrawCheckmark(SKCanvas canvas, SKRect bounds) + { + using var paint = new SKPaint + { + Color = new SKColor(0x21, 0x96, 0xF3), + Style = SKPaintStyle.Stroke, + StrokeWidth = 2, + IsAntialias = true, + StrokeCap = SKStrokeCap.Round + }; + + using var path = new SKPath(); + path.MoveTo(bounds.Left, bounds.MidY); + path.LineTo(bounds.MidX - 2, bounds.Bottom - 2); + path.LineTo(bounds.Right, bounds.Top + 2); + + canvas.DrawPath(path, paint); + } + + protected override void OnDraw(SKCanvas canvas, SKRect bounds) + { + // Reset the heights-changed flag at the start of each draw + _heightsChangedDuringDraw = false; + + // Draw background if set + if (BackgroundColor != SKColors.Transparent) + { + using var bgPaint = new SKPaint + { + Color = BackgroundColor, + Style = SKPaintStyle.Fill + }; + canvas.DrawRect(bounds, bgPaint); + } + + // Draw header if present + if (Header != null && HeaderHeight > 0) + { + var headerRect = new SKRect(bounds.Left, bounds.Top, bounds.Right, bounds.Top + HeaderHeight); + DrawHeader(canvas, headerRect); + } + + // Draw footer if present + if (Footer != null && FooterHeight > 0) + { + var footerRect = new SKRect(bounds.Left, bounds.Bottom - FooterHeight, bounds.Right, bounds.Bottom); + DrawFooter(canvas, footerRect); + } + + // Adjust content bounds for header/footer + var contentBounds = new SKRect( + bounds.Left, + bounds.Top + HeaderHeight, + bounds.Right, + bounds.Bottom - FooterHeight); + + // Draw items + if (ItemCount == 0) + { + DrawEmptyView(canvas, contentBounds); + return; + } + + // Use grid layout if spanCount > 1 + if (SpanCount > 1) + { + DrawGridItems(canvas, contentBounds); + } + else + { + DrawListItems(canvas, contentBounds); + } + + // If heights changed during this draw, schedule a redraw with correct positions + // This will queue another frame to be drawn with the correct cached heights + if (_heightsChangedDuringDraw) + { + _heightsChangedDuringDraw = false; + Invalidate(); + } + } + + private void DrawListItems(SKCanvas canvas, SKRect bounds) + { + // Standard list drawing with variable item heights + canvas.Save(); + canvas.ClipRect(bounds); + + using var paint = new SKPaint { IsAntialias = true }; + + var scrollOffset = GetScrollOffset(); + + // Find first visible item by walking through items + int firstVisible = 0; + float cumulativeOffset = 0; + for (int i = 0; i < ItemCount; i++) + { + var itemH = GetItemHeight(i); + if (cumulativeOffset + itemH > scrollOffset) + { + firstVisible = i; + break; + } + cumulativeOffset += itemH + ItemSpacing; + } + + // Draw visible items using variable heights + float currentY = bounds.Top + GetItemOffset(firstVisible) - scrollOffset; + for (int i = firstVisible; i < ItemCount; i++) + { + var itemH = GetItemHeight(i); + var itemRect = new SKRect(bounds.Left, currentY, bounds.Right - 8, currentY + itemH); + + // Stop if we've passed the visible area + if (itemRect.Top > bounds.Bottom) + break; + + if (itemRect.Bottom >= bounds.Top) + { + var item = GetItemAt(i); + if (item != null) + { + DrawItem(canvas, item, i, itemRect, paint); + } + } + + currentY += itemH + ItemSpacing; + } + + canvas.Restore(); + + // Draw scrollbar + var totalHeight = TotalContentHeight; + if (totalHeight > bounds.Height) + { + DrawScrollBarInternal(canvas, bounds, scrollOffset, totalHeight); + } + } + + private void DrawGridItems(SKCanvas canvas, SKRect bounds) + { + canvas.Save(); + canvas.ClipRect(bounds); + + using var paint = new SKPaint { IsAntialias = true }; + + var cellWidth = (bounds.Width - 8) / SpanCount; // -8 for scrollbar + var cellHeight = ItemHeight; + var rowCount = (int)Math.Ceiling((double)ItemCount / SpanCount); + var totalHeight = rowCount * (cellHeight + ItemSpacing) - ItemSpacing; + + var scrollOffset = GetScrollOffset(); + var firstVisibleRow = Math.Max(0, (int)(scrollOffset / (cellHeight + ItemSpacing))); + var lastVisibleRow = Math.Min(rowCount - 1, + (int)((scrollOffset + bounds.Height) / (cellHeight + ItemSpacing)) + 1); + + for (int row = firstVisibleRow; row <= lastVisibleRow; row++) + { + var rowY = bounds.Top + (row * (cellHeight + ItemSpacing)) - scrollOffset; + + for (int col = 0; col < SpanCount; col++) + { + var index = row * SpanCount + col; + if (index >= ItemCount) break; + + var cellX = bounds.Left + col * cellWidth; + var cellRect = new SKRect(cellX + 2, rowY, cellX + cellWidth - 2, rowY + cellHeight); + + if (cellRect.Bottom < bounds.Top || cellRect.Top > bounds.Bottom) + continue; + + var item = GetItemAt(index); + if (item != null) + { + // Draw cell background + using var cellBgPaint = new SKPaint + { + Color = _selectedItems.Contains(item) ? SelectionColor : new SKColor(0xFA, 0xFA, 0xFA), + Style = SKPaintStyle.Fill + }; + canvas.DrawRoundRect(new SKRoundRect(cellRect, 4), cellBgPaint); + + DrawItem(canvas, item, index, cellRect, paint); + } + } + } + + canvas.Restore(); + + // Draw scrollbar + if (totalHeight > bounds.Height) + { + DrawScrollBarInternal(canvas, bounds, scrollOffset, totalHeight); + } + } + + private void DrawScrollBarInternal(SKCanvas canvas, SKRect bounds, float scrollOffset, float totalHeight) + { + var scrollBarWidth = 6f; + var scrollBarMargin = 2f; + + // Draw scrollbar track (subtle) + var trackRect = new SKRect( + bounds.Right - scrollBarWidth - scrollBarMargin, + bounds.Top + scrollBarMargin, + bounds.Right - scrollBarMargin, + bounds.Bottom - scrollBarMargin); + + using var trackPaint = new SKPaint + { + Color = new SKColor(0, 0, 0, 20), + Style = SKPaintStyle.Fill + }; + canvas.DrawRoundRect(new SKRoundRect(trackRect, 3), trackPaint); + + // Calculate thumb position and size + var maxOffset = Math.Max(0, totalHeight - bounds.Height); + var viewportRatio = bounds.Height / totalHeight; + var availableTrackHeight = trackRect.Height; + var thumbHeight = Math.Max(30, availableTrackHeight * viewportRatio); + var scrollRatio = maxOffset > 0 ? scrollOffset / maxOffset : 0; + var thumbY = trackRect.Top + (availableTrackHeight - thumbHeight) * scrollRatio; + + var thumbRect = new SKRect( + trackRect.Left, + thumbY, + trackRect.Right, + thumbY + thumbHeight); + + // Draw thumb with more visible color + using var thumbPaint = new SKPaint + { + Color = new SKColor(100, 100, 100, 180), + Style = SKPaintStyle.Fill, + IsAntialias = true + }; + + canvas.DrawRoundRect(new SKRoundRect(thumbRect, 3), thumbPaint); + } + + private float GetScrollOffset() + { + // Access base class scroll offset through reflection or expose it + // For now, use the field directly through internal access + return _scrollOffset; + } + + private void DrawHeader(SKCanvas canvas, SKRect bounds) + { + using var bgPaint = new SKPaint + { + Color = HeaderBackgroundColor, + Style = SKPaintStyle.Fill + }; + canvas.DrawRect(bounds, bgPaint); + + // Draw header text + var text = Header.ToString() ?? ""; + if (!string.IsNullOrEmpty(text)) + { + using var font = new SKFont(SKTypeface.Default, 16); + using var textPaint = new SKPaint(font) + { + Color = SKColors.Black, + IsAntialias = true + }; + + var textBounds = new SKRect(); + textPaint.MeasureText(text, ref textBounds); + + var x = bounds.Left + 16; + var y = bounds.MidY - textBounds.MidY; + canvas.DrawText(text, x, y, textPaint); + } + + // Draw separator + using var sepPaint = new SKPaint + { + Color = new SKColor(0xE0, 0xE0, 0xE0), + Style = SKPaintStyle.Stroke, + StrokeWidth = 1 + }; + canvas.DrawLine(bounds.Left, bounds.Bottom, bounds.Right, bounds.Bottom, sepPaint); + } + + private void DrawFooter(SKCanvas canvas, SKRect bounds) + { + using var bgPaint = new SKPaint + { + Color = FooterBackgroundColor, + Style = SKPaintStyle.Fill + }; + canvas.DrawRect(bounds, bgPaint); + + // Draw separator + using var sepPaint = new SKPaint + { + Color = new SKColor(0xE0, 0xE0, 0xE0), + Style = SKPaintStyle.Stroke, + StrokeWidth = 1 + }; + canvas.DrawLine(bounds.Left, bounds.Top, bounds.Right, bounds.Top, sepPaint); + + // Draw footer text + var text = Footer.ToString() ?? ""; + if (!string.IsNullOrEmpty(text)) + { + using var font = new SKFont(SKTypeface.Default, 14); + using var textPaint = new SKPaint(font) + { + Color = new SKColor(0x80, 0x80, 0x80), + IsAntialias = true + }; + + var textBounds = new SKRect(); + textPaint.MeasureText(text, ref textBounds); + + var x = bounds.MidX - textBounds.MidX; + var y = bounds.MidY - textBounds.MidY; + canvas.DrawText(text, x, y, textPaint); + } + } +} + +/// +/// Event args for collection selection changed events. +/// +public class CollectionSelectionChangedEventArgs : EventArgs +{ + public IReadOnlyList PreviousSelection { get; } + public IReadOnlyList CurrentSelection { get; } + + public CollectionSelectionChangedEventArgs(IList previousSelection, IList currentSelection) + { + PreviousSelection = previousSelection.ToList().AsReadOnly(); + CurrentSelection = currentSelection.ToList().AsReadOnly(); + } } diff --git a/Views/SkiaContentPage.cs b/Views/SkiaContentPage.cs deleted file mode 100644 index 963e757..0000000 --- a/Views/SkiaContentPage.cs +++ /dev/null @@ -1,207 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using SkiaSharp; - -namespace Microsoft.Maui.Platform; - -public class SkiaContentPage : SkiaPage -{ - private readonly List _toolbarItems = new List(); - - public IList ToolbarItems => _toolbarItems; - - protected override void DrawNavigationBar(SKCanvas canvas, SKRect bounds) - { - //IL_0000: Unknown result type (might be due to invalid IL or missing references) - //IL_0005: Unknown result type (might be due to invalid IL or missing references) - //IL_0007: Unknown result type (might be due to invalid IL or missing references) - //IL_0011: Unknown result type (might be due to invalid IL or missing references) - //IL_0019: Expected O, but got Unknown - //IL_001a: Unknown result type (might be due to invalid IL or missing references) - //IL_00c5: Unknown result type (might be due to invalid IL or missing references) - //IL_00cb: Unknown result type (might be due to invalid IL or missing references) - //IL_00d0: Unknown result type (might be due to invalid IL or missing references) - //IL_00d6: Unknown result type (might be due to invalid IL or missing references) - //IL_00e0: Unknown result type (might be due to invalid IL or missing references) - //IL_00e7: Unknown result type (might be due to invalid IL or missing references) - //IL_00f9: Expected O, but got Unknown - //IL_0045: Unknown result type (might be due to invalid IL or missing references) - //IL_004b: Expected O, but got Unknown - //IL_011c: Unknown result type (might be due to invalid IL or missing references) - //IL_004c: Unknown result type (might be due to invalid IL or missing references) - //IL_0051: Unknown result type (might be due to invalid IL or missing references) - //IL_0053: Unknown result type (might be due to invalid IL or missing references) - //IL_005d: Unknown result type (might be due to invalid IL or missing references) - //IL_0065: Expected O, but got Unknown - //IL_0067: Unknown result type (might be due to invalid IL or missing references) - SKPaint val = new SKPaint - { - Color = base.TitleBarColor, - Style = (SKPaintStyle)0 - }; - try - { - canvas.DrawRect(bounds, val); - if (!string.IsNullOrEmpty(base.Title)) - { - SKFont val2 = new SKFont(SKTypeface.Default, 20f, 1f, 0f); - try - { - SKPaint val3 = new SKPaint(val2) - { - Color = base.TitleTextColor, - IsAntialias = true - }; - try - { - SKRect val4 = default(SKRect); - val3.MeasureText(base.Title, ref val4); - float num = ((SKRect)(ref bounds)).Left + 56f; - float num2 = ((SKRect)(ref bounds)).MidY - ((SKRect)(ref val4)).MidY; - canvas.DrawText(base.Title, num, num2, val3); - } - finally - { - ((IDisposable)val3)?.Dispose(); - } - } - finally - { - ((IDisposable)val2)?.Dispose(); - } - } - DrawToolbarItems(canvas, bounds); - SKPaint val5 = new SKPaint - { - Color = new SKColor((byte)0, (byte)0, (byte)0, (byte)30), - Style = (SKPaintStyle)0, - MaskFilter = SKMaskFilter.CreateBlur((SKBlurStyle)0, 2f) - }; - try - { - canvas.DrawRect(new SKRect(((SKRect)(ref bounds)).Left, ((SKRect)(ref bounds)).Bottom, ((SKRect)(ref bounds)).Right, ((SKRect)(ref bounds)).Bottom + 4f), val5); - } - finally - { - ((IDisposable)val5)?.Dispose(); - } - } - finally - { - ((IDisposable)val)?.Dispose(); - } - } - - private void DrawToolbarItems(SKCanvas canvas, SKRect navBarBounds) - { - //IL_0061: Unknown result type (might be due to invalid IL or missing references) - //IL_0090: Unknown result type (might be due to invalid IL or missing references) - //IL_0096: Expected O, but got Unknown - //IL_0097: Unknown result type (might be due to invalid IL or missing references) - //IL_009c: Unknown result type (might be due to invalid IL or missing references) - //IL_009e: Unknown result type (might be due to invalid IL or missing references) - //IL_00a8: Unknown result type (might be due to invalid IL or missing references) - //IL_00b0: Expected O, but got Unknown - //IL_0183: Unknown result type (might be due to invalid IL or missing references) - //IL_01c1: Unknown result type (might be due to invalid IL or missing references) - //IL_010a: Unknown result type (might be due to invalid IL or missing references) - //IL_0151: Unknown result type (might be due to invalid IL or missing references) - //IL_0156: Unknown result type (might be due to invalid IL or missing references) - //IL_015f: Expected O, but got Unknown - //IL_022c: Unknown result type (might be due to invalid IL or missing references) - //IL_0167: Unknown result type (might be due to invalid IL or missing references) - List list = _toolbarItems.Where((SkiaToolbarItem t) => t.Order == SkiaToolbarItemOrder.Primary).ToList(); - Console.WriteLine($"[SkiaContentPage] DrawToolbarItems: {list.Count} primary items, navBarBounds={navBarBounds}"); - if (list.Count == 0) - { - return; - } - SKFont val = new SKFont(SKTypeface.Default, 14f, 1f, 0f); - try - { - SKPaint val2 = new SKPaint(val) - { - Color = base.TitleTextColor, - IsAntialias = true - }; - try - { - float num = ((SKRect)(ref navBarBounds)).Right - 16f; - SKRect val3 = default(SKRect); - foreach (SkiaToolbarItem item in list.AsEnumerable().Reverse()) - { - float num3; - if (item.Icon != null) - { - float num2 = 40f; - num3 = num - num2; - item.HitBounds = new SKRect(num3, ((SKRect)(ref navBarBounds)).Top, num, ((SKRect)(ref navBarBounds)).Bottom); - float num4 = num3 + (num2 - 24f) / 2f; - float num5 = ((SKRect)(ref navBarBounds)).MidY - 12f; - ((SKRect)(ref val3))._002Ector(num4, num5, num4 + 24f, num5 + 24f); - SKPaint val4 = new SKPaint - { - IsAntialias = true - }; - try - { - canvas.DrawBitmap(item.Icon, val3, val4); - } - finally - { - ((IDisposable)val4)?.Dispose(); - } - } - else - { - SKRect val5 = default(SKRect); - val2.MeasureText(item.Text, ref val5); - float num2 = ((SKRect)(ref val5)).Width + 24f; - num3 = num - num2; - item.HitBounds = new SKRect(num3, ((SKRect)(ref navBarBounds)).Top, num, ((SKRect)(ref navBarBounds)).Bottom); - float num6 = num3 + 12f; - float num7 = ((SKRect)(ref navBarBounds)).MidY - ((SKRect)(ref val5)).MidY; - canvas.DrawText(item.Text, num6, num7, val2); - } - Console.WriteLine($"[SkiaContentPage] Toolbar item '{item.Text}' HitBounds set to {item.HitBounds}"); - num = num3 - 8f; - } - } - finally - { - ((IDisposable)val2)?.Dispose(); - } - } - finally - { - ((IDisposable)val)?.Dispose(); - } - } - - public override void OnPointerPressed(PointerEventArgs e) - { - //IL_0111: Unknown result type (might be due to invalid IL or missing references) - //IL_0116: Unknown result type (might be due to invalid IL or missing references) - Console.WriteLine($"[SkiaContentPage] OnPointerPressed at ({e.X}, {e.Y}), ShowNavigationBar={base.ShowNavigationBar}, NavigationBarHeight={base.NavigationBarHeight}"); - Console.WriteLine($"[SkiaContentPage] ToolbarItems count: {_toolbarItems.Count}"); - if (base.ShowNavigationBar && e.Y < base.NavigationBarHeight) - { - Console.WriteLine("[SkiaContentPage] In navigation bar area, checking toolbar items"); - foreach (SkiaToolbarItem item in _toolbarItems.Where((SkiaToolbarItem t) => t.Order == SkiaToolbarItemOrder.Primary)) - { - SKRect hitBounds = item.HitBounds; - bool flag = ((SKRect)(ref hitBounds)).Contains(e.X, e.Y); - Console.WriteLine($"[SkiaContentPage] Checking item '{item.Text}', HitBounds=({((SKRect)(ref hitBounds)).Left},{((SKRect)(ref hitBounds)).Top},{((SKRect)(ref hitBounds)).Right},{((SKRect)(ref hitBounds)).Bottom}), Click=({e.X},{e.Y}), Contains={flag}, Command={item.Command != null}"); - if (flag) - { - Console.WriteLine("[SkiaContentPage] Toolbar item clicked: " + item.Text); - item.Command?.Execute(null); - return; - } - } - Console.WriteLine("[SkiaContentPage] No toolbar item hit"); - } - base.OnPointerPressed(e); - } -} diff --git a/Views/SkiaContentPresenter.cs b/Views/SkiaContentPresenter.cs index 67da40b..4c38c78 100644 --- a/Views/SkiaContentPresenter.cs +++ b/Views/SkiaContentPresenter.cs @@ -1,242 +1,257 @@ -using System; -using Microsoft.Maui.Controls; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using SkiaSharp; namespace Microsoft.Maui.Platform; +/// +/// Presents content within a ControlTemplate. +/// This control acts as a placeholder that gets replaced with the actual content +/// when the template is applied to a control. +/// public class SkiaContentPresenter : SkiaView { - public static readonly BindableProperty ContentProperty = BindableProperty.Create("Content", typeof(SkiaView), typeof(SkiaContentPresenter), (object)null, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaContentPresenter)(object)b).OnContentChanged((SkiaView)o, (SkiaView)n); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + #region BindableProperties - public static readonly BindableProperty HorizontalContentAlignmentProperty = BindableProperty.Create("HorizontalContentAlignment", typeof(LayoutAlignment), typeof(SkiaContentPresenter), (object)LayoutAlignment.Fill, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaContentPresenter)(object)b).InvalidateMeasure(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + public static readonly BindableProperty ContentProperty = + BindableProperty.Create(nameof(Content), typeof(SkiaView), typeof(SkiaContentPresenter), null, + propertyChanged: (b, o, n) => ((SkiaContentPresenter)b).OnContentChanged((SkiaView?)o, (SkiaView?)n)); - public static readonly BindableProperty VerticalContentAlignmentProperty = BindableProperty.Create("VerticalContentAlignment", typeof(LayoutAlignment), typeof(SkiaContentPresenter), (object)LayoutAlignment.Fill, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaContentPresenter)(object)b).InvalidateMeasure(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + public static readonly BindableProperty HorizontalContentAlignmentProperty = + BindableProperty.Create(nameof(HorizontalContentAlignment), typeof(LayoutAlignment), typeof(SkiaContentPresenter), LayoutAlignment.Fill, + propertyChanged: (b, o, n) => ((SkiaContentPresenter)b).InvalidateMeasure()); - public static readonly BindableProperty PaddingProperty = BindableProperty.Create("Padding", typeof(SKRect), typeof(SkiaContentPresenter), (object)SKRect.Empty, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaContentPresenter)(object)b).InvalidateMeasure(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + public static readonly BindableProperty VerticalContentAlignmentProperty = + BindableProperty.Create(nameof(VerticalContentAlignment), typeof(LayoutAlignment), typeof(SkiaContentPresenter), LayoutAlignment.Fill, + propertyChanged: (b, o, n) => ((SkiaContentPresenter)b).InvalidateMeasure()); - public SkiaView? Content - { - get - { - return (SkiaView)((BindableObject)this).GetValue(ContentProperty); - } - set - { - ((BindableObject)this).SetValue(ContentProperty, (object)value); - } - } + public static readonly BindableProperty PaddingProperty = + BindableProperty.Create(nameof(Padding), typeof(SKRect), typeof(SkiaContentPresenter), SKRect.Empty, + propertyChanged: (b, o, n) => ((SkiaContentPresenter)b).InvalidateMeasure()); - public LayoutAlignment HorizontalContentAlignment - { - get - { - return (LayoutAlignment)((BindableObject)this).GetValue(HorizontalContentAlignmentProperty); - } - set - { - ((BindableObject)this).SetValue(HorizontalContentAlignmentProperty, (object)value); - } - } + #endregion - public LayoutAlignment VerticalContentAlignment - { - get - { - return (LayoutAlignment)((BindableObject)this).GetValue(VerticalContentAlignmentProperty); - } - set - { - ((BindableObject)this).SetValue(VerticalContentAlignmentProperty, (object)value); - } - } + #region Properties - public SKRect Padding - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKRect)((BindableObject)this).GetValue(PaddingProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(PaddingProperty, (object)value); - } - } + /// + /// Gets or sets the content to present. + /// + public SkiaView? Content + { + get => (SkiaView?)GetValue(ContentProperty); + set => SetValue(ContentProperty, value); + } - private void OnContentChanged(SkiaView? oldContent, SkiaView? newContent) - { - if (oldContent != null) - { - oldContent.Parent = null; - } - if (newContent != null) - { - newContent.Parent = this; - if (((BindableObject)this).BindingContext != null) - { - BindableObject.SetInheritedBindingContext((BindableObject)(object)newContent, ((BindableObject)this).BindingContext); - } - } - InvalidateMeasure(); - } + /// + /// Gets or sets the horizontal alignment of the content. + /// + public LayoutAlignment HorizontalContentAlignment + { + get => (LayoutAlignment)GetValue(HorizontalContentAlignmentProperty); + set => SetValue(HorizontalContentAlignmentProperty, value); + } - protected override void OnBindingContextChanged() - { - base.OnBindingContextChanged(); - if (Content != null) - { - BindableObject.SetInheritedBindingContext((BindableObject)(object)Content, ((BindableObject)this).BindingContext); - } - } + /// + /// Gets or sets the vertical alignment of the content. + /// + public LayoutAlignment VerticalContentAlignment + { + get => (LayoutAlignment)GetValue(VerticalContentAlignmentProperty); + set => SetValue(VerticalContentAlignmentProperty, value); + } - protected override void OnDraw(SKCanvas canvas, SKRect bounds) - { - //IL_0001: Unknown result type (might be due to invalid IL or missing references) - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - //IL_0012: Unknown result type (might be due to invalid IL or missing references) - //IL_0017: Unknown result type (might be due to invalid IL or missing references) - //IL_0019: Unknown result type (might be due to invalid IL or missing references) - //IL_0023: Unknown result type (might be due to invalid IL or missing references) - //IL_002b: Expected O, but got Unknown - //IL_002c: Unknown result type (might be due to invalid IL or missing references) - if (base.BackgroundColor != SKColors.Transparent) - { - SKPaint val = new SKPaint - { - Color = base.BackgroundColor, - Style = (SKPaintStyle)0 - }; - try - { - canvas.DrawRect(bounds, val); - } - finally - { - ((IDisposable)val)?.Dispose(); - } - } - Content?.Draw(canvas); - } + /// + /// Gets or sets the padding around the content. + /// + public SKRect Padding + { + get => (SKRect)GetValue(PaddingProperty); + set => SetValue(PaddingProperty, value); + } - protected override SKSize MeasureOverride(SKSize availableSize) - { - //IL_0001: Unknown result type (might be due to invalid IL or missing references) - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - //IL_002d: Unknown result type (might be due to invalid IL or missing references) - //IL_009d: Unknown result type (might be due to invalid IL or missing references) - //IL_00a2: Unknown result type (might be due to invalid IL or missing references) - //IL_00a7: Unknown result type (might be due to invalid IL or missing references) - //IL_00d6: Unknown result type (might be due to invalid IL or missing references) - SKRect padding = Padding; - if (Content == null) - { - return new SKSize(((SKRect)(ref padding)).Left + ((SKRect)(ref padding)).Right, ((SKRect)(ref padding)).Top + ((SKRect)(ref padding)).Bottom); - } - float num = ((HorizontalContentAlignment == LayoutAlignment.Fill) ? Math.Max(0f, ((SKSize)(ref availableSize)).Width - ((SKRect)(ref padding)).Left - ((SKRect)(ref padding)).Right) : float.PositiveInfinity); - float num2 = ((VerticalContentAlignment == LayoutAlignment.Fill) ? Math.Max(0f, ((SKSize)(ref availableSize)).Height - ((SKRect)(ref padding)).Top - ((SKRect)(ref padding)).Bottom) : float.PositiveInfinity); - SKSize val = Content.Measure(new SKSize(num, num2)); - return new SKSize(((SKSize)(ref val)).Width + ((SKRect)(ref padding)).Left + ((SKRect)(ref padding)).Right, ((SKSize)(ref val)).Height + ((SKRect)(ref padding)).Top + ((SKRect)(ref padding)).Bottom); - } + #endregion - protected override SKRect ArrangeOverride(SKRect bounds) - { - //IL_007b: Unknown result type (might be due to invalid IL or missing references) - //IL_0009: Unknown result type (might be due to invalid IL or missing references) - //IL_000e: Unknown result type (might be due to invalid IL or missing references) - //IL_004b: Unknown result type (might be due to invalid IL or missing references) - //IL_0056: Unknown result type (might be due to invalid IL or missing references) - //IL_005b: Unknown result type (might be due to invalid IL or missing references) - //IL_005c: Unknown result type (might be due to invalid IL or missing references) - //IL_0069: Unknown result type (might be due to invalid IL or missing references) - //IL_006e: Unknown result type (might be due to invalid IL or missing references) - //IL_0075: Unknown result type (might be due to invalid IL or missing references) - if (Content != null) - { - SKRect padding = Padding; - SKRect availableBounds = new SKRect(((SKRect)(ref bounds)).Left + ((SKRect)(ref padding)).Left, ((SKRect)(ref bounds)).Top + ((SKRect)(ref padding)).Top, ((SKRect)(ref bounds)).Right - ((SKRect)(ref padding)).Right, ((SKRect)(ref bounds)).Bottom - ((SKRect)(ref padding)).Bottom); - SKSize desiredSize = Content.DesiredSize; - SKRect bounds2 = ApplyAlignment(availableBounds, desiredSize, HorizontalContentAlignment, VerticalContentAlignment); - Content.Arrange(bounds2); - } - return bounds; - } + private void OnContentChanged(SkiaView? oldContent, SkiaView? newContent) + { + if (oldContent != null) + { + oldContent.Parent = null; + } - private static SKRect ApplyAlignment(SKRect availableBounds, SKSize contentSize, LayoutAlignment horizontal, LayoutAlignment vertical) - { - //IL_009c: Unknown result type (might be due to invalid IL or missing references) - float num = ((SKRect)(ref availableBounds)).Left; - float num2 = ((SKRect)(ref availableBounds)).Top; - float num3 = ((horizontal == LayoutAlignment.Fill) ? ((SKRect)(ref availableBounds)).Width : ((SKSize)(ref contentSize)).Width); - float num4 = ((vertical == LayoutAlignment.Fill) ? ((SKRect)(ref availableBounds)).Height : ((SKSize)(ref contentSize)).Height); - switch (horizontal) - { - case LayoutAlignment.Center: - num = ((SKRect)(ref availableBounds)).Left + (((SKRect)(ref availableBounds)).Width - num3) / 2f; - break; - case LayoutAlignment.End: - num = ((SKRect)(ref availableBounds)).Right - num3; - break; - } - switch (vertical) - { - case LayoutAlignment.Center: - num2 = ((SKRect)(ref availableBounds)).Top + (((SKRect)(ref availableBounds)).Height - num4) / 2f; - break; - case LayoutAlignment.End: - num2 = ((SKRect)(ref availableBounds)).Bottom - num4; - break; - } - return new SKRect(num, num2, num + num3, num2 + num4); - } + if (newContent != null) + { + newContent.Parent = this; - public override SkiaView? HitTest(float x, float y) - { - //IL_0009: Unknown result type (might be due to invalid IL or missing references) - //IL_000e: Unknown result type (might be due to invalid IL or missing references) - if (base.IsVisible) - { - SKRect bounds = base.Bounds; - if (((SKRect)(ref bounds)).Contains(x, y)) - { - if (Content != null) - { - SkiaView skiaView = Content.HitTest(x, y); - if (skiaView != null) - { - return skiaView; - } - } - return this; - } - } - return null; - } + // Propagate binding context to new content + if (BindingContext != null) + { + SetInheritedBindingContext(newContent, BindingContext); + } + } - public override void OnPointerPressed(PointerEventArgs e) - { - Content?.OnPointerPressed(e); - } + InvalidateMeasure(); + } - public override void OnPointerMoved(PointerEventArgs e) - { - Content?.OnPointerMoved(e); - } + /// + /// Called when binding context changes. Propagates to content. + /// + protected override void OnBindingContextChanged() + { + base.OnBindingContextChanged(); - public override void OnPointerReleased(PointerEventArgs e) - { - Content?.OnPointerReleased(e); - } + // Propagate binding context to content + if (Content != null) + { + SetInheritedBindingContext(Content, BindingContext); + } + } + + protected override void OnDraw(SKCanvas canvas, SKRect bounds) + { + // Draw background if set + if (BackgroundColor != SKColors.Transparent) + { + using var bgPaint = new SKPaint + { + Color = BackgroundColor, + Style = SKPaintStyle.Fill + }; + canvas.DrawRect(bounds, bgPaint); + } + + // Draw content + Content?.Draw(canvas); + } + + protected override SKSize MeasureOverride(SKSize availableSize) + { + var padding = Padding; + + if (Content == null) + return new SKSize(padding.Left + padding.Right, padding.Top + padding.Bottom); + + // When alignment is not Fill, give content unlimited size in that dimension + // so it can measure its natural size without truncation + var measureWidth = HorizontalContentAlignment == LayoutAlignment.Fill + ? Math.Max(0, availableSize.Width - padding.Left - padding.Right) + : float.PositiveInfinity; + var measureHeight = VerticalContentAlignment == LayoutAlignment.Fill + ? Math.Max(0, availableSize.Height - padding.Top - padding.Bottom) + : float.PositiveInfinity; + + var contentSize = Content.Measure(new SKSize(measureWidth, measureHeight)); + return new SKSize( + contentSize.Width + padding.Left + padding.Right, + contentSize.Height + padding.Top + padding.Bottom); + } + + protected override SKRect ArrangeOverride(SKRect bounds) + { + if (Content != null) + { + var padding = Padding; + var contentBounds = new SKRect( + bounds.Left + padding.Left, + bounds.Top + padding.Top, + bounds.Right - padding.Right, + bounds.Bottom - padding.Bottom); + + // Apply alignment + var contentSize = Content.DesiredSize; + var arrangedBounds = ApplyAlignment(contentBounds, contentSize, HorizontalContentAlignment, VerticalContentAlignment); + Content.Arrange(arrangedBounds); + } + + return bounds; + } + + private static SKRect ApplyAlignment(SKRect availableBounds, SKSize contentSize, LayoutAlignment horizontal, LayoutAlignment vertical) + { + float x = availableBounds.Left; + float y = availableBounds.Top; + float width = horizontal == LayoutAlignment.Fill ? availableBounds.Width : contentSize.Width; + float height = vertical == LayoutAlignment.Fill ? availableBounds.Height : contentSize.Height; + + // Horizontal alignment + switch (horizontal) + { + case LayoutAlignment.Center: + x = availableBounds.Left + (availableBounds.Width - width) / 2; + break; + case LayoutAlignment.End: + x = availableBounds.Right - width; + break; + } + + // Vertical alignment + switch (vertical) + { + case LayoutAlignment.Center: + y = availableBounds.Top + (availableBounds.Height - height) / 2; + break; + case LayoutAlignment.End: + y = availableBounds.Bottom - height; + break; + } + + return new SKRect(x, y, x + width, y + height); + } + + public override SkiaView? HitTest(float x, float y) + { + if (!IsVisible || !Bounds.Contains(x, y)) + return null; + + // Check content first + if (Content != null) + { + var hit = Content.HitTest(x, y); + if (hit != null) + return hit; + } + + return this; + } + + public override void OnPointerPressed(PointerEventArgs e) + { + Content?.OnPointerPressed(e); + } + + public override void OnPointerMoved(PointerEventArgs e) + { + Content?.OnPointerMoved(e); + } + + public override void OnPointerReleased(PointerEventArgs e) + { + Content?.OnPointerReleased(e); + } +} + +/// +/// Layout alignment options. +/// +public enum LayoutAlignment +{ + /// + /// Fill the available space. + /// + Fill, + + /// + /// Align to the start (left or top). + /// + Start, + + /// + /// Align to the center. + /// + Center, + + /// + /// Align to the end (right or bottom). + /// + End } diff --git a/Views/SkiaContextMenu.cs b/Views/SkiaContextMenu.cs deleted file mode 100644 index 4a8a67e..0000000 --- a/Views/SkiaContextMenu.cs +++ /dev/null @@ -1,312 +0,0 @@ -using System; -using System.Collections.Generic; -using SkiaSharp; - -namespace Microsoft.Maui.Platform; - -public class SkiaContextMenu : SkiaView -{ - private readonly List _items; - - private readonly float _x; - - private readonly float _y; - - private int _hoveredIndex = -1; - - private SKRect[] _itemBounds = Array.Empty(); - - private static readonly SKColor MenuBackground = new SKColor(byte.MaxValue, byte.MaxValue, byte.MaxValue); - - private static readonly SKColor MenuBackgroundDark = new SKColor((byte)48, (byte)48, (byte)48); - - private static readonly SKColor ItemHoverBackground = new SKColor((byte)227, (byte)242, (byte)253); - - private static readonly SKColor ItemHoverBackgroundDark = new SKColor((byte)80, (byte)80, (byte)80); - - private static readonly SKColor ItemTextColor = new SKColor((byte)33, (byte)33, (byte)33); - - private static readonly SKColor ItemTextColorDark = new SKColor((byte)224, (byte)224, (byte)224); - - private static readonly SKColor DisabledTextColor = new SKColor((byte)158, (byte)158, (byte)158); - - private static readonly SKColor SeparatorColor = new SKColor((byte)224, (byte)224, (byte)224); - - private static readonly SKColor ShadowColor = new SKColor((byte)0, (byte)0, (byte)0, (byte)40); - - private const float MenuPadding = 4f; - - private const float ItemHeight = 32f; - - private const float ItemPaddingH = 16f; - - private const float SeparatorHeight = 9f; - - private const float CornerRadius = 4f; - - private const float MinWidth = 120f; - - private bool _isDarkTheme; - - public SkiaContextMenu(float x, float y, List items, bool isDarkTheme = false) - { - _x = x; - _y = y; - _items = items; - _isDarkTheme = isDarkTheme; - base.IsFocusable = true; - } - - public override void Draw(SKCanvas canvas) - { - //IL_0070: Unknown result type (might be due to invalid IL or missing references) - //IL_0075: Unknown result type (might be due to invalid IL or missing references) - //IL_0076: Unknown result type (might be due to invalid IL or missing references) - //IL_0080: Unknown result type (might be due to invalid IL or missing references) - //IL_0093: Expected O, but got Unknown - //IL_00c1: Unknown result type (might be due to invalid IL or missing references) - //IL_00c6: Unknown result type (might be due to invalid IL or missing references) - //IL_00d6: Unknown result type (might be due to invalid IL or missing references) - //IL_00cf: Unknown result type (might be due to invalid IL or missing references) - //IL_00e0: Unknown result type (might be due to invalid IL or missing references) - //IL_00e9: Expected O, but got Unknown - //IL_00ea: Unknown result type (might be due to invalid IL or missing references) - //IL_00fd: Unknown result type (might be due to invalid IL or missing references) - //IL_0102: Unknown result type (might be due to invalid IL or missing references) - //IL_0103: Unknown result type (might be due to invalid IL or missing references) - //IL_010d: Unknown result type (might be due to invalid IL or missing references) - //IL_0114: Unknown result type (might be due to invalid IL or missing references) - //IL_011f: Unknown result type (might be due to invalid IL or missing references) - //IL_0128: Expected O, but got Unknown - //IL_0129: Unknown result type (might be due to invalid IL or missing references) - //IL_0222: Unknown result type (might be due to invalid IL or missing references) - //IL_0224: Unknown result type (might be due to invalid IL or missing references) - //IL_0185: Unknown result type (might be due to invalid IL or missing references) - //IL_018a: Unknown result type (might be due to invalid IL or missing references) - //IL_018b: Unknown result type (might be due to invalid IL or missing references) - //IL_0195: Unknown result type (might be due to invalid IL or missing references) - //IL_01a2: Expected O, but got Unknown - //IL_0286: Unknown result type (might be due to invalid IL or missing references) - //IL_028b: Unknown result type (might be due to invalid IL or missing references) - //IL_01d4: Unknown result type (might be due to invalid IL or missing references) - //IL_01d9: Unknown result type (might be due to invalid IL or missing references) - //IL_0295: Unknown result type (might be due to invalid IL or missing references) - //IL_023c: Unknown result type (might be due to invalid IL or missing references) - //IL_0241: Unknown result type (might be due to invalid IL or missing references) - //IL_02ab: Unknown result type (might be due to invalid IL or missing references) - //IL_02a4: Unknown result type (might be due to invalid IL or missing references) - //IL_02b5: Unknown result type (might be due to invalid IL or missing references) - //IL_02c0: Unknown result type (might be due to invalid IL or missing references) - //IL_02c7: Unknown result type (might be due to invalid IL or missing references) - //IL_02d4: Expected O, but got Unknown - //IL_0251: Unknown result type (might be due to invalid IL or missing references) - //IL_024a: Unknown result type (might be due to invalid IL or missing references) - //IL_025b: Unknown result type (might be due to invalid IL or missing references) - //IL_0264: Expected O, but got Unknown - //IL_0265: Unknown result type (might be due to invalid IL or missing references) - float num = CalculateMenuWidth(); - float num2 = CalculateMenuHeight(); - float num3 = _x; - float num4 = _y; - SKRectI val = default(SKRectI); - canvas.GetDeviceClipBounds(ref val); - if (num3 + num > (float)((SKRectI)(ref val)).Right) - { - num3 = (float)((SKRectI)(ref val)).Right - num - 4f; - } - if (num4 + num2 > (float)((SKRectI)(ref val)).Bottom) - { - num4 = (float)((SKRectI)(ref val)).Bottom - num2 - 4f; - } - SKRect val2 = default(SKRect); - ((SKRect)(ref val2))._002Ector(num3, num4, num3 + num, num4 + num2); - SKPaint val3 = new SKPaint - { - Color = ShadowColor, - MaskFilter = SKMaskFilter.CreateBlur((SKBlurStyle)0, 4f) - }; - try - { - canvas.DrawRoundRect(((SKRect)(ref val2)).Left + 2f, ((SKRect)(ref val2)).Top + 2f, num, num2, 4f, 4f, val3); - SKPaint val4 = new SKPaint - { - Color = (_isDarkTheme ? MenuBackgroundDark : MenuBackground), - IsAntialias = true - }; - try - { - canvas.DrawRoundRect(val2, 4f, 4f, val4); - SKPaint val5 = new SKPaint - { - Color = SeparatorColor, - Style = (SKPaintStyle)1, - StrokeWidth = 1f, - IsAntialias = true - }; - try - { - canvas.DrawRoundRect(val2, 4f, 4f, val5); - _itemBounds = (SKRect[])(object)new SKRect[_items.Count]; - float num5 = num4 + 4f; - SKRect val7 = default(SKRect); - for (int i = 0; i < _items.Count; i++) - { - ContextMenuItem contextMenuItem = _items[i]; - if (contextMenuItem.IsSeparator) - { - float num6 = num5 + 4.5f; - SKPaint val6 = new SKPaint - { - Color = SeparatorColor, - StrokeWidth = 1f - }; - try - { - canvas.DrawLine(num3 + 8f, num6, num3 + num - 8f, num6, val6); - _itemBounds[i] = new SKRect(num3, num5, num3 + num, num5 + 9f); - num5 += 9f; - } - finally - { - ((IDisposable)val6)?.Dispose(); - } - continue; - } - ((SKRect)(ref val7))._002Ector(num3 + 4f, num5, num3 + num - 4f, num5 + 32f); - _itemBounds[i] = val7; - if (i == _hoveredIndex && contextMenuItem.IsEnabled) - { - SKPaint val8 = new SKPaint - { - Color = (_isDarkTheme ? ItemHoverBackgroundDark : ItemHoverBackground), - IsAntialias = true - }; - try - { - canvas.DrawRoundRect(val7, 4f, 4f, val8); - } - finally - { - ((IDisposable)val8)?.Dispose(); - } - } - SKPaint val9 = new SKPaint - { - Color = ((!contextMenuItem.IsEnabled) ? DisabledTextColor : (_isDarkTheme ? ItemTextColorDark : ItemTextColor)), - TextSize = 14f, - IsAntialias = true, - Typeface = SKTypeface.Default - }; - try - { - float num7 = ((SKRect)(ref val7)).MidY + val9.TextSize / 3f; - canvas.DrawText(contextMenuItem.Text, ((SKRect)(ref val7)).Left + 16f, num7, val9); - num5 += 32f; - } - finally - { - ((IDisposable)val9)?.Dispose(); - } - } - } - finally - { - ((IDisposable)val5)?.Dispose(); - } - } - finally - { - ((IDisposable)val4)?.Dispose(); - } - } - finally - { - ((IDisposable)val3)?.Dispose(); - } - } - - private float CalculateMenuWidth() - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - //IL_0016: Unknown result type (might be due to invalid IL or missing references) - //IL_0022: Expected O, but got Unknown - float num = 120f; - SKPaint val = new SKPaint - { - TextSize = 14f, - Typeface = SKTypeface.Default - }; - try - { - foreach (ContextMenuItem item in _items) - { - if (!item.IsSeparator) - { - float val2 = val.MeasureText(item.Text) + 32f; - num = Math.Max(num, val2); - } - } - return num + 8f; - } - finally - { - ((IDisposable)val)?.Dispose(); - } - } - - private float CalculateMenuHeight() - { - float num = 8f; - foreach (ContextMenuItem item in _items) - { - num += (item.IsSeparator ? 9f : 32f); - } - return num; - } - - public override void OnPointerMoved(PointerEventArgs e) - { - int hoveredIndex = _hoveredIndex; - _hoveredIndex = -1; - for (int i = 0; i < _itemBounds.Length; i++) - { - if (((SKRect)(ref _itemBounds[i])).Contains(e.X, e.Y) && !_items[i].IsSeparator) - { - _hoveredIndex = i; - break; - } - } - if (hoveredIndex != _hoveredIndex) - { - Invalidate(); - } - } - - public override void OnPointerPressed(PointerEventArgs e) - { - for (int i = 0; i < _itemBounds.Length; i++) - { - if (((SKRect)(ref _itemBounds[i])).Contains(e.X, e.Y)) - { - ContextMenuItem contextMenuItem = _items[i]; - if (contextMenuItem.IsEnabled && !contextMenuItem.IsSeparator && contextMenuItem.Action != null) - { - LinuxDialogService.HideContextMenu(); - contextMenuItem.Action(); - return; - } - } - } - LinuxDialogService.HideContextMenu(); - } - - public override void OnKeyDown(KeyEventArgs e) - { - if (e.Key == Key.Escape) - { - LinuxDialogService.HideContextMenu(); - e.Handled = true; - } - } -} diff --git a/Views/SkiaDatePicker.cs b/Views/SkiaDatePicker.cs index b7146f9..163adaa 100644 --- a/Views/SkiaDatePicker.cs +++ b/Views/SkiaDatePicker.cs @@ -1,963 +1,510 @@ -using System; -using Microsoft.Maui.Controls; -using Microsoft.Maui.Platform.Linux; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using SkiaSharp; +using Microsoft.Maui.Platform.Linux; namespace Microsoft.Maui.Platform; +/// +/// Skia-rendered date picker control with calendar popup. +/// public class SkiaDatePicker : SkiaView { - public static readonly BindableProperty DateProperty = BindableProperty.Create("Date", typeof(DateTime), typeof(SkiaDatePicker), (object)DateTime.Today, (BindingMode)1, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaDatePicker)(object)b).OnDatePropertyChanged(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + #region BindableProperties - public static readonly BindableProperty MinimumDateProperty = BindableProperty.Create("MinimumDate", typeof(DateTime), typeof(SkiaDatePicker), (object)new DateTime(1900, 1, 1), (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaDatePicker)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + public static readonly BindableProperty DateProperty = + BindableProperty.Create(nameof(Date), typeof(DateTime), typeof(SkiaDatePicker), DateTime.Today, BindingMode.TwoWay, + propertyChanged: (b, o, n) => ((SkiaDatePicker)b).OnDatePropertyChanged()); - public static readonly BindableProperty MaximumDateProperty = BindableProperty.Create("MaximumDate", typeof(DateTime), typeof(SkiaDatePicker), (object)new DateTime(2100, 12, 31), (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaDatePicker)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + public static readonly BindableProperty MinimumDateProperty = + BindableProperty.Create(nameof(MinimumDate), typeof(DateTime), typeof(SkiaDatePicker), new DateTime(1900, 1, 1), + propertyChanged: (b, o, n) => ((SkiaDatePicker)b).Invalidate()); - public static readonly BindableProperty FormatProperty = BindableProperty.Create("Format", typeof(string), typeof(SkiaDatePicker), (object)"d", (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaDatePicker)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + public static readonly BindableProperty MaximumDateProperty = + BindableProperty.Create(nameof(MaximumDate), typeof(DateTime), typeof(SkiaDatePicker), new DateTime(2100, 12, 31), + propertyChanged: (b, o, n) => ((SkiaDatePicker)b).Invalidate()); - public static readonly BindableProperty TextColorProperty = BindableProperty.Create("TextColor", typeof(SKColor), typeof(SkiaDatePicker), (object)SKColors.Black, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaDatePicker)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + public static readonly BindableProperty FormatProperty = + BindableProperty.Create(nameof(Format), typeof(string), typeof(SkiaDatePicker), "d", + propertyChanged: (b, o, n) => ((SkiaDatePicker)b).Invalidate()); - public static readonly BindableProperty BorderColorProperty = BindableProperty.Create("BorderColor", typeof(SKColor), typeof(SkiaDatePicker), (object)new SKColor((byte)189, (byte)189, (byte)189), (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaDatePicker)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + public static readonly BindableProperty TextColorProperty = + BindableProperty.Create(nameof(TextColor), typeof(SKColor), typeof(SkiaDatePicker), SKColors.Black, + propertyChanged: (b, o, n) => ((SkiaDatePicker)b).Invalidate()); - public static readonly BindableProperty CalendarBackgroundColorProperty = BindableProperty.Create("CalendarBackgroundColor", typeof(SKColor), typeof(SkiaDatePicker), (object)SKColors.White, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaDatePicker)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + public static readonly BindableProperty BorderColorProperty = + BindableProperty.Create(nameof(BorderColor), typeof(SKColor), typeof(SkiaDatePicker), new SKColor(0xBD, 0xBD, 0xBD), + propertyChanged: (b, o, n) => ((SkiaDatePicker)b).Invalidate()); - public static readonly BindableProperty SelectedDayColorProperty = BindableProperty.Create("SelectedDayColor", typeof(SKColor), typeof(SkiaDatePicker), (object)new SKColor((byte)33, (byte)150, (byte)243), (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaDatePicker)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + public static readonly BindableProperty CalendarBackgroundColorProperty = + BindableProperty.Create(nameof(CalendarBackgroundColor), typeof(SKColor), typeof(SkiaDatePicker), SKColors.White, + propertyChanged: (b, o, n) => ((SkiaDatePicker)b).Invalidate()); - public static readonly BindableProperty TodayColorProperty = BindableProperty.Create("TodayColor", typeof(SKColor), typeof(SkiaDatePicker), (object)new SKColor((byte)33, (byte)150, (byte)243, (byte)64), (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaDatePicker)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + public static readonly BindableProperty SelectedDayColorProperty = + BindableProperty.Create(nameof(SelectedDayColor), typeof(SKColor), typeof(SkiaDatePicker), new SKColor(0x21, 0x96, 0xF3), + propertyChanged: (b, o, n) => ((SkiaDatePicker)b).Invalidate()); - public static readonly BindableProperty HeaderColorProperty = BindableProperty.Create("HeaderColor", typeof(SKColor), typeof(SkiaDatePicker), (object)new SKColor((byte)33, (byte)150, (byte)243), (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaDatePicker)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + public static readonly BindableProperty TodayColorProperty = + BindableProperty.Create(nameof(TodayColor), typeof(SKColor), typeof(SkiaDatePicker), new SKColor(0x21, 0x96, 0xF3, 0x40), + propertyChanged: (b, o, n) => ((SkiaDatePicker)b).Invalidate()); - public static readonly BindableProperty DisabledDayColorProperty = BindableProperty.Create("DisabledDayColor", typeof(SKColor), typeof(SkiaDatePicker), (object)new SKColor((byte)189, (byte)189, (byte)189), (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaDatePicker)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + public static readonly BindableProperty HeaderColorProperty = + BindableProperty.Create(nameof(HeaderColor), typeof(SKColor), typeof(SkiaDatePicker), new SKColor(0x21, 0x96, 0xF3), + propertyChanged: (b, o, n) => ((SkiaDatePicker)b).Invalidate()); - public static readonly BindableProperty FontSizeProperty = BindableProperty.Create("FontSize", typeof(float), typeof(SkiaDatePicker), (object)14f, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaDatePicker)(object)b).InvalidateMeasure(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + public static readonly BindableProperty DisabledDayColorProperty = + BindableProperty.Create(nameof(DisabledDayColor), typeof(SKColor), typeof(SkiaDatePicker), new SKColor(0xBD, 0xBD, 0xBD), + propertyChanged: (b, o, n) => ((SkiaDatePicker)b).Invalidate()); - public static readonly BindableProperty CornerRadiusProperty = BindableProperty.Create("CornerRadius", typeof(float), typeof(SkiaDatePicker), (object)4f, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaDatePicker)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + public static readonly BindableProperty FontSizeProperty = + BindableProperty.Create(nameof(FontSize), typeof(float), typeof(SkiaDatePicker), 14f, + propertyChanged: (b, o, n) => ((SkiaDatePicker)b).InvalidateMeasure()); - private DateTime _displayMonth; + public static readonly BindableProperty CornerRadiusProperty = + BindableProperty.Create(nameof(CornerRadius), typeof(float), typeof(SkiaDatePicker), 4f, + propertyChanged: (b, o, n) => ((SkiaDatePicker)b).Invalidate()); - private bool _isOpen; + #endregion - private const float CalendarWidth = 280f; + #region Properties - private const float CalendarHeight = 320f; + public DateTime Date + { + get => (DateTime)GetValue(DateProperty); + set => SetValue(DateProperty, ClampDate(value)); + } - private const float HeaderHeight = 48f; + public DateTime MinimumDate + { + get => (DateTime)GetValue(MinimumDateProperty); + set => SetValue(MinimumDateProperty, value); + } - public DateTime Date - { - get - { - return (DateTime)((BindableObject)this).GetValue(DateProperty); - } - set - { - ((BindableObject)this).SetValue(DateProperty, (object)ClampDate(value)); - } - } + public DateTime MaximumDate + { + get => (DateTime)GetValue(MaximumDateProperty); + set => SetValue(MaximumDateProperty, value); + } - public DateTime MinimumDate - { - get - { - return (DateTime)((BindableObject)this).GetValue(MinimumDateProperty); - } - set - { - ((BindableObject)this).SetValue(MinimumDateProperty, (object)value); - } - } + public string Format + { + get => (string)GetValue(FormatProperty); + set => SetValue(FormatProperty, value); + } - public DateTime MaximumDate - { - get - { - return (DateTime)((BindableObject)this).GetValue(MaximumDateProperty); - } - set - { - ((BindableObject)this).SetValue(MaximumDateProperty, (object)value); - } - } + public SKColor TextColor + { + get => (SKColor)GetValue(TextColorProperty); + set => SetValue(TextColorProperty, value); + } - public string Format - { - get - { - return (string)((BindableObject)this).GetValue(FormatProperty); - } - set - { - ((BindableObject)this).SetValue(FormatProperty, (object)value); - } - } + public SKColor BorderColor + { + get => (SKColor)GetValue(BorderColorProperty); + set => SetValue(BorderColorProperty, value); + } - public SKColor TextColor - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKColor)((BindableObject)this).GetValue(TextColorProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(TextColorProperty, (object)value); - } - } + public SKColor CalendarBackgroundColor + { + get => (SKColor)GetValue(CalendarBackgroundColorProperty); + set => SetValue(CalendarBackgroundColorProperty, value); + } - public SKColor BorderColor - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKColor)((BindableObject)this).GetValue(BorderColorProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(BorderColorProperty, (object)value); - } - } + public SKColor SelectedDayColor + { + get => (SKColor)GetValue(SelectedDayColorProperty); + set => SetValue(SelectedDayColorProperty, value); + } - public SKColor CalendarBackgroundColor - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKColor)((BindableObject)this).GetValue(CalendarBackgroundColorProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(CalendarBackgroundColorProperty, (object)value); - } - } + public SKColor TodayColor + { + get => (SKColor)GetValue(TodayColorProperty); + set => SetValue(TodayColorProperty, value); + } - public SKColor SelectedDayColor - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKColor)((BindableObject)this).GetValue(SelectedDayColorProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(SelectedDayColorProperty, (object)value); - } - } + public SKColor HeaderColor + { + get => (SKColor)GetValue(HeaderColorProperty); + set => SetValue(HeaderColorProperty, value); + } - public SKColor TodayColor - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKColor)((BindableObject)this).GetValue(TodayColorProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(TodayColorProperty, (object)value); - } - } + public SKColor DisabledDayColor + { + get => (SKColor)GetValue(DisabledDayColorProperty); + set => SetValue(DisabledDayColorProperty, value); + } - public SKColor HeaderColor - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKColor)((BindableObject)this).GetValue(HeaderColorProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(HeaderColorProperty, (object)value); - } - } + public float FontSize + { + get => (float)GetValue(FontSizeProperty); + set => SetValue(FontSizeProperty, value); + } - public SKColor DisabledDayColor - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKColor)((BindableObject)this).GetValue(DisabledDayColorProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(DisabledDayColorProperty, (object)value); - } - } + public float CornerRadius + { + get => (float)GetValue(CornerRadiusProperty); + set => SetValue(CornerRadiusProperty, value); + } - public float FontSize - { - get - { - return (float)((BindableObject)this).GetValue(FontSizeProperty); - } - set - { - ((BindableObject)this).SetValue(FontSizeProperty, (object)value); - } - } + public bool IsOpen + { + get => _isOpen; + set + { + if (_isOpen != value) + { + _isOpen = value; + if (_isOpen) + RegisterPopupOverlay(this, DrawCalendarOverlay); + else + UnregisterPopupOverlay(this); + Invalidate(); + } + } + } - public float CornerRadius - { - get - { - return (float)((BindableObject)this).GetValue(CornerRadiusProperty); - } - set - { - ((BindableObject)this).SetValue(CornerRadiusProperty, (object)value); - } - } + #endregion - public bool IsOpen - { - get - { - return _isOpen; - } - set - { - if (_isOpen != value) - { - _isOpen = value; - if (_isOpen) - { - SkiaView.RegisterPopupOverlay(this, DrawCalendarOverlay); - } - else - { - SkiaView.UnregisterPopupOverlay(this); - } - Invalidate(); - } - } - } + private DateTime _displayMonth; + private bool _isOpen; - public event EventHandler? DateSelected; + private const float CalendarWidth = 280; + private const float CalendarHeight = 320; + private const float HeaderHeight = 48; - private SKRect GetCalendarRect(SKRect pickerBounds) - { - //IL_0101: Unknown result type (might be due to invalid IL or missing references) - int num = LinuxApplication.Current?.MainWindow?.Width ?? 800; - int num2 = LinuxApplication.Current?.MainWindow?.Height ?? 600; - float num3 = ((SKRect)(ref pickerBounds)).Left; - float num4 = ((SKRect)(ref pickerBounds)).Bottom + 4f; - if (num3 + 280f > (float)num) - { - num3 = (float)num - 280f - 4f; - } - if (num3 < 0f) - { - num3 = 4f; - } - if (num4 + 320f > (float)num2) - { - num4 = ((SKRect)(ref pickerBounds)).Top - 320f - 4f; - } - if (num4 < 0f) - { - num4 = 4f; - } - return new SKRect(num3, num4, num3 + 280f, num4 + 320f); - } + public event EventHandler? DateSelected; - public SkiaDatePicker() - { - base.IsFocusable = true; - _displayMonth = new DateTime(DateTime.Today.Year, DateTime.Today.Month, 1); - } + /// + /// Gets the calendar popup rectangle with edge detection applied. + /// + private SKRect GetCalendarRect(SKRect pickerBounds) + { + // Get window dimensions for edge detection + var windowWidth = LinuxApplication.Current?.MainWindow?.Width ?? 800; + var windowHeight = LinuxApplication.Current?.MainWindow?.Height ?? 600; - private void OnDatePropertyChanged() - { - _displayMonth = new DateTime(Date.Year, Date.Month, 1); - this.DateSelected?.Invoke(this, EventArgs.Empty); - Invalidate(); - } + // Calculate default position (below the picker) + var calendarLeft = pickerBounds.Left; + var calendarTop = pickerBounds.Bottom + 4; - private DateTime ClampDate(DateTime date) - { - if (date < MinimumDate) - { - return MinimumDate; - } - if (date > MaximumDate) - { - return MaximumDate; - } - return date; - } + // Edge detection: adjust horizontal position if popup would go off-screen + if (calendarLeft + CalendarWidth > windowWidth) + { + calendarLeft = windowWidth - CalendarWidth - 4; + } + if (calendarLeft < 0) calendarLeft = 4; - private void DrawCalendarOverlay(SKCanvas canvas) - { - //IL_000c: Unknown result type (might be due to invalid IL or missing references) - if (_isOpen) - { - DrawCalendar(canvas, base.ScreenBounds); - } - } + // Edge detection: show above if popup would go off-screen vertically + if (calendarTop + CalendarHeight > windowHeight) + { + calendarTop = pickerBounds.Top - CalendarHeight - 4; + } + if (calendarTop < 0) calendarTop = 4; - protected override void OnDraw(SKCanvas canvas, SKRect bounds) - { - //IL_0002: Unknown result type (might be due to invalid IL or missing references) - DrawPickerButton(canvas, bounds); - } + return new SKRect(calendarLeft, calendarTop, calendarLeft + CalendarWidth, calendarTop + CalendarHeight); + } - private void DrawPickerButton(SKCanvas canvas, SKRect bounds) - { - //IL_0000: Unknown result type (might be due to invalid IL or missing references) - //IL_0005: Unknown result type (might be due to invalid IL or missing references) - //IL_0025: Unknown result type (might be due to invalid IL or missing references) - //IL_001d: Unknown result type (might be due to invalid IL or missing references) - //IL_002f: Unknown result type (might be due to invalid IL or missing references) - //IL_0036: Unknown result type (might be due to invalid IL or missing references) - //IL_003e: Expected O, but got Unknown - //IL_003f: Unknown result type (might be due to invalid IL or missing references) - //IL_0046: Unknown result type (might be due to invalid IL or missing references) - //IL_0051: Expected O, but got Unknown - //IL_0051: Unknown result type (might be due to invalid IL or missing references) - //IL_0056: Unknown result type (might be due to invalid IL or missing references) - //IL_0068: Unknown result type (might be due to invalid IL or missing references) - //IL_0060: Unknown result type (might be due to invalid IL or missing references) - //IL_0072: Unknown result type (might be due to invalid IL or missing references) - //IL_0079: Unknown result type (might be due to invalid IL or missing references) - //IL_008c: Unknown result type (might be due to invalid IL or missing references) - //IL_0094: Expected O, but got Unknown - //IL_0095: Unknown result type (might be due to invalid IL or missing references) - //IL_009c: Unknown result type (might be due to invalid IL or missing references) - //IL_00a7: Expected O, but got Unknown - //IL_00bc: Unknown result type (might be due to invalid IL or missing references) - //IL_00c2: Expected O, but got Unknown - //IL_00c3: Unknown result type (might be due to invalid IL or missing references) - //IL_00c8: Unknown result type (might be due to invalid IL or missing references) - //IL_00e8: Unknown result type (might be due to invalid IL or missing references) - //IL_00d2: Unknown result type (might be due to invalid IL or missing references) - //IL_00d7: Unknown result type (might be due to invalid IL or missing references) - //IL_00e0: Unknown result type (might be due to invalid IL or missing references) - //IL_00f2: Unknown result type (might be due to invalid IL or missing references) - //IL_00fa: Expected O, but got Unknown - //IL_0113: Unknown result type (might be due to invalid IL or missing references) - //IL_017f: Unknown result type (might be due to invalid IL or missing references) - SKPaint val = new SKPaint - { - Color = (SKColor)(base.IsEnabled ? base.BackgroundColor : new SKColor((byte)245, (byte)245, (byte)245)), - Style = (SKPaintStyle)0, - IsAntialias = true - }; - try - { - canvas.DrawRoundRect(new SKRoundRect(bounds, CornerRadius), val); - SKPaint val2 = new SKPaint - { - Color = (base.IsFocused ? SelectedDayColor : BorderColor), - Style = (SKPaintStyle)1, - StrokeWidth = ((!base.IsFocused) ? 1 : 2), - IsAntialias = true - }; - try - { - canvas.DrawRoundRect(new SKRoundRect(bounds, CornerRadius), val2); - SKFont val3 = new SKFont(SKTypeface.Default, FontSize, 1f, 0f); - try - { - SKPaint val4 = new SKPaint(val3); - SKColor color; - if (!base.IsEnabled) - { - SKColor textColor = TextColor; - color = ((SKColor)(ref textColor)).WithAlpha((byte)128); - } - else - { - color = TextColor; - } - val4.Color = color; - val4.IsAntialias = true; - SKPaint val5 = val4; - try - { - string text = Date.ToString(Format); - SKRect val6 = default(SKRect); - val5.MeasureText(text, ref val6); - canvas.DrawText(text, ((SKRect)(ref bounds)).Left + 12f, ((SKRect)(ref bounds)).MidY - ((SKRect)(ref val6)).MidY, val5); - DrawCalendarIcon(canvas, new SKRect(((SKRect)(ref bounds)).Right - 36f, ((SKRect)(ref bounds)).MidY - 10f, ((SKRect)(ref bounds)).Right - 12f, ((SKRect)(ref bounds)).MidY + 10f)); - } - finally - { - ((IDisposable)val5)?.Dispose(); - } - } - finally - { - ((IDisposable)val3)?.Dispose(); - } - } - finally - { - ((IDisposable)val2)?.Dispose(); - } - } - finally - { - ((IDisposable)val)?.Dispose(); - } - } + public SkiaDatePicker() + { + IsFocusable = true; + _displayMonth = new DateTime(DateTime.Today.Year, DateTime.Today.Month, 1); + } - private void DrawCalendarIcon(SKCanvas canvas, SKRect bounds) - { - //IL_0000: Unknown result type (might be due to invalid IL or missing references) - //IL_0005: Unknown result type (might be due to invalid IL or missing references) - //IL_0024: Unknown result type (might be due to invalid IL or missing references) - //IL_000f: Unknown result type (might be due to invalid IL or missing references) - //IL_0014: Unknown result type (might be due to invalid IL or missing references) - //IL_001c: Unknown result type (might be due to invalid IL or missing references) - //IL_002e: Unknown result type (might be due to invalid IL or missing references) - //IL_0035: Unknown result type (might be due to invalid IL or missing references) - //IL_0040: Unknown result type (might be due to invalid IL or missing references) - //IL_0048: Expected O, but got Unknown - //IL_006a: Unknown result type (might be due to invalid IL or missing references) - //IL_006f: Unknown result type (might be due to invalid IL or missing references) - //IL_0071: Unknown result type (might be due to invalid IL or missing references) - //IL_0077: Unknown result type (might be due to invalid IL or missing references) - //IL_0082: Expected O, but got Unknown - SKPaint val = new SKPaint(); - SKColor color; - if (!base.IsEnabled) - { - SKColor textColor = TextColor; - color = ((SKColor)(ref textColor)).WithAlpha((byte)128); - } - else - { - color = TextColor; - } - val.Color = color; - val.Style = (SKPaintStyle)1; - val.StrokeWidth = 1.5f; - val.IsAntialias = true; - SKPaint val2 = val; - try - { - SKRect val3 = new SKRect(((SKRect)(ref bounds)).Left, ((SKRect)(ref bounds)).Top + 3f, ((SKRect)(ref bounds)).Right, ((SKRect)(ref bounds)).Bottom); - canvas.DrawRoundRect(new SKRoundRect(val3, 2f), val2); - canvas.DrawLine(((SKRect)(ref bounds)).Left + 5f, ((SKRect)(ref bounds)).Top, ((SKRect)(ref bounds)).Left + 5f, ((SKRect)(ref bounds)).Top + 5f, val2); - canvas.DrawLine(((SKRect)(ref bounds)).Right - 5f, ((SKRect)(ref bounds)).Top, ((SKRect)(ref bounds)).Right - 5f, ((SKRect)(ref bounds)).Top + 5f, val2); - canvas.DrawLine(((SKRect)(ref bounds)).Left, ((SKRect)(ref bounds)).Top + 8f, ((SKRect)(ref bounds)).Right, ((SKRect)(ref bounds)).Top + 8f, val2); - val2.Style = (SKPaintStyle)0; - for (int i = 0; i < 2; i++) - { - for (int j = 0; j < 3; j++) - { - canvas.DrawCircle(((SKRect)(ref bounds)).Left + 4f + (float)(j * 6), ((SKRect)(ref bounds)).Top + 12f + (float)(i * 4), 1f, val2); - } - } - } - finally - { - ((IDisposable)val2)?.Dispose(); - } - } + private void OnDatePropertyChanged() + { + _displayMonth = new DateTime(Date.Year, Date.Month, 1); + DateSelected?.Invoke(this, EventArgs.Empty); + Invalidate(); + } - private void DrawCalendar(SKCanvas canvas, SKRect bounds) - { - //IL_0001: Unknown result type (might be due to invalid IL or missing references) - //IL_0002: Unknown result type (might be due to invalid IL or missing references) - //IL_0007: Unknown result type (might be due to invalid IL or missing references) - //IL_0008: Unknown result type (might be due to invalid IL or missing references) - //IL_000d: Unknown result type (might be due to invalid IL or missing references) - //IL_0013: Unknown result type (might be due to invalid IL or missing references) - //IL_001d: Unknown result type (might be due to invalid IL or missing references) - //IL_002e: Unknown result type (might be due to invalid IL or missing references) - //IL_0036: Expected O, but got Unknown - //IL_006b: Unknown result type (might be due to invalid IL or missing references) - //IL_0076: Unknown result type (might be due to invalid IL or missing references) - //IL_0081: Expected O, but got Unknown - //IL_0081: Unknown result type (might be due to invalid IL or missing references) - //IL_0086: Unknown result type (might be due to invalid IL or missing references) - //IL_0088: Unknown result type (might be due to invalid IL or missing references) - //IL_0092: Unknown result type (might be due to invalid IL or missing references) - //IL_0099: Unknown result type (might be due to invalid IL or missing references) - //IL_00a1: Expected O, but got Unknown - //IL_00a2: Unknown result type (might be due to invalid IL or missing references) - //IL_00a9: Unknown result type (might be due to invalid IL or missing references) - //IL_00b4: Expected O, but got Unknown - //IL_00b4: Unknown result type (might be due to invalid IL or missing references) - //IL_00b9: Unknown result type (might be due to invalid IL or missing references) - //IL_00bb: Unknown result type (might be due to invalid IL or missing references) - //IL_00c5: Unknown result type (might be due to invalid IL or missing references) - //IL_00cc: Unknown result type (might be due to invalid IL or missing references) - //IL_00d7: Unknown result type (might be due to invalid IL or missing references) - //IL_00df: Expected O, but got Unknown - //IL_00e0: Unknown result type (might be due to invalid IL or missing references) - //IL_00e7: Unknown result type (might be due to invalid IL or missing references) - //IL_00f2: Expected O, but got Unknown - //IL_0116: Unknown result type (might be due to invalid IL or missing references) - //IL_0150: Unknown result type (might be due to invalid IL or missing references) - //IL_0184: Unknown result type (might be due to invalid IL or missing references) - SKRect calendarRect = GetCalendarRect(bounds); - SKPaint val = new SKPaint - { - Color = new SKColor((byte)0, (byte)0, (byte)0, (byte)40), - MaskFilter = SKMaskFilter.CreateBlur((SKBlurStyle)0, 4f), - Style = (SKPaintStyle)0 - }; - try - { - canvas.DrawRoundRect(new SKRoundRect(new SKRect(((SKRect)(ref calendarRect)).Left + 2f, ((SKRect)(ref calendarRect)).Top + 2f, ((SKRect)(ref calendarRect)).Right + 2f, ((SKRect)(ref calendarRect)).Bottom + 2f), CornerRadius), val); - SKPaint val2 = new SKPaint - { - Color = CalendarBackgroundColor, - Style = (SKPaintStyle)0, - IsAntialias = true - }; - try - { - canvas.DrawRoundRect(new SKRoundRect(calendarRect, CornerRadius), val2); - SKPaint val3 = new SKPaint - { - Color = BorderColor, - Style = (SKPaintStyle)1, - StrokeWidth = 1f, - IsAntialias = true - }; - try - { - canvas.DrawRoundRect(new SKRoundRect(calendarRect, CornerRadius), val3); - DrawCalendarHeader(canvas, new SKRect(((SKRect)(ref calendarRect)).Left, ((SKRect)(ref calendarRect)).Top, ((SKRect)(ref calendarRect)).Right, ((SKRect)(ref calendarRect)).Top + 48f)); - DrawWeekdayHeaders(canvas, new SKRect(((SKRect)(ref calendarRect)).Left, ((SKRect)(ref calendarRect)).Top + 48f, ((SKRect)(ref calendarRect)).Right, ((SKRect)(ref calendarRect)).Top + 48f + 30f)); - DrawDays(canvas, new SKRect(((SKRect)(ref calendarRect)).Left, ((SKRect)(ref calendarRect)).Top + 48f + 30f, ((SKRect)(ref calendarRect)).Right, ((SKRect)(ref calendarRect)).Bottom)); - } - finally - { - ((IDisposable)val3)?.Dispose(); - } - } - finally - { - ((IDisposable)val2)?.Dispose(); - } - } - finally - { - ((IDisposable)val)?.Dispose(); - } - } + private DateTime ClampDate(DateTime date) + { + if (date < MinimumDate) return MinimumDate; + if (date > MaximumDate) return MaximumDate; + return date; + } - private void DrawCalendarHeader(SKCanvas canvas, SKRect bounds) - { - //IL_0000: Unknown result type (might be due to invalid IL or missing references) - //IL_0005: Unknown result type (might be due to invalid IL or missing references) - //IL_0007: Unknown result type (might be due to invalid IL or missing references) - //IL_0011: Unknown result type (might be due to invalid IL or missing references) - //IL_0019: Expected O, but got Unknown - //IL_004a: Unknown result type (might be due to invalid IL or missing references) - //IL_0055: Unknown result type (might be due to invalid IL or missing references) - //IL_0061: Expected O, but got Unknown - //IL_0062: Unknown result type (might be due to invalid IL or missing references) - //IL_0093: Unknown result type (might be due to invalid IL or missing references) - //IL_00b2: Unknown result type (might be due to invalid IL or missing references) - //IL_00b8: Expected O, but got Unknown - //IL_00b9: Unknown result type (might be due to invalid IL or missing references) - //IL_00be: Unknown result type (might be due to invalid IL or missing references) - //IL_00bf: Unknown result type (might be due to invalid IL or missing references) - //IL_00c9: Unknown result type (might be due to invalid IL or missing references) - //IL_00d1: Expected O, but got Unknown - //IL_00e4: Unknown result type (might be due to invalid IL or missing references) - //IL_011a: Unknown result type (might be due to invalid IL or missing references) - //IL_011f: Unknown result type (might be due to invalid IL or missing references) - //IL_0120: Unknown result type (might be due to invalid IL or missing references) - //IL_012a: Unknown result type (might be due to invalid IL or missing references) - //IL_0131: Unknown result type (might be due to invalid IL or missing references) - //IL_013c: Unknown result type (might be due to invalid IL or missing references) - //IL_0143: Unknown result type (might be due to invalid IL or missing references) - //IL_014c: Expected O, but got Unknown - //IL_014c: Unknown result type (might be due to invalid IL or missing references) - //IL_0153: Expected O, but got Unknown - //IL_01ba: Unknown result type (might be due to invalid IL or missing references) - //IL_01c1: Expected O, but got Unknown - SKPaint val = new SKPaint - { - Color = HeaderColor, - Style = (SKPaintStyle)0 - }; - try - { - canvas.Save(); - canvas.ClipRoundRect(new SKRoundRect(new SKRect(((SKRect)(ref bounds)).Left, ((SKRect)(ref bounds)).Top, ((SKRect)(ref bounds)).Right, ((SKRect)(ref bounds)).Top + CornerRadius * 2f), CornerRadius), (SKClipOperation)1, false); - canvas.DrawRect(bounds, val); - canvas.Restore(); - canvas.DrawRect(new SKRect(((SKRect)(ref bounds)).Left, ((SKRect)(ref bounds)).Top + CornerRadius, ((SKRect)(ref bounds)).Right, ((SKRect)(ref bounds)).Bottom), val); - SKFont val2 = new SKFont(SKTypeface.Default, 16f, 1f, 0f); - try - { - SKPaint val3 = new SKPaint(val2) - { - Color = SKColors.White, - IsAntialias = true - }; - try - { - string text = _displayMonth.ToString("MMMM yyyy"); - SKRect val4 = default(SKRect); - val3.MeasureText(text, ref val4); - canvas.DrawText(text, ((SKRect)(ref bounds)).MidX - ((SKRect)(ref val4)).MidX, ((SKRect)(ref bounds)).MidY - ((SKRect)(ref val4)).MidY, val3); - SKPaint val5 = new SKPaint - { - Color = SKColors.White, - Style = (SKPaintStyle)1, - StrokeWidth = 2f, - IsAntialias = true, - StrokeCap = (SKStrokeCap)1 - }; - try - { - SKPath val6 = new SKPath(); - try - { - val6.MoveTo(((SKRect)(ref bounds)).Left + 26f, ((SKRect)(ref bounds)).MidY - 6f); - val6.LineTo(((SKRect)(ref bounds)).Left + 20f, ((SKRect)(ref bounds)).MidY); - val6.LineTo(((SKRect)(ref bounds)).Left + 26f, ((SKRect)(ref bounds)).MidY + 6f); - canvas.DrawPath(val6, val5); - SKPath val7 = new SKPath(); - try - { - val7.MoveTo(((SKRect)(ref bounds)).Right - 26f, ((SKRect)(ref bounds)).MidY - 6f); - val7.LineTo(((SKRect)(ref bounds)).Right - 20f, ((SKRect)(ref bounds)).MidY); - val7.LineTo(((SKRect)(ref bounds)).Right - 26f, ((SKRect)(ref bounds)).MidY + 6f); - canvas.DrawPath(val7, val5); - } - finally - { - ((IDisposable)val7)?.Dispose(); - } - } - finally - { - ((IDisposable)val6)?.Dispose(); - } - } - finally - { - ((IDisposable)val5)?.Dispose(); - } - } - finally - { - ((IDisposable)val3)?.Dispose(); - } - } - finally - { - ((IDisposable)val2)?.Dispose(); - } - } - finally - { - ((IDisposable)val)?.Dispose(); - } - } + private void DrawCalendarOverlay(SKCanvas canvas) + { + if (!_isOpen) return; + // Use ScreenBounds for popup drawing (accounts for scroll offset) + DrawCalendar(canvas, ScreenBounds); + } - private void DrawWeekdayHeaders(SKCanvas canvas, SKRect bounds) - { - //IL_0061: Unknown result type (might be due to invalid IL or missing references) - //IL_0067: Expected O, but got Unknown - //IL_0068: Unknown result type (might be due to invalid IL or missing references) - //IL_006d: Unknown result type (might be due to invalid IL or missing references) - //IL_007d: Unknown result type (might be due to invalid IL or missing references) - //IL_0087: Unknown result type (might be due to invalid IL or missing references) - //IL_008f: Expected O, but got Unknown - //IL_0096: Unknown result type (might be due to invalid IL or missing references) - string[] array = new string[7] { "Su", "Mo", "Tu", "We", "Th", "Fr", "Sa" }; - float num = ((SKRect)(ref bounds)).Width / 7f; - SKFont val = new SKFont(SKTypeface.Default, 12f, 1f, 0f); - try - { - SKPaint val2 = new SKPaint(val) - { - Color = new SKColor((byte)128, (byte)128, (byte)128), - IsAntialias = true - }; - try - { - for (int i = 0; i < 7; i++) - { - SKRect val3 = default(SKRect); - val2.MeasureText(array[i], ref val3); - canvas.DrawText(array[i], ((SKRect)(ref bounds)).Left + (float)i * num + num / 2f - ((SKRect)(ref val3)).MidX, ((SKRect)(ref bounds)).MidY - ((SKRect)(ref val3)).MidY, val2); - } - } - finally - { - ((IDisposable)val2)?.Dispose(); - } - } - finally - { - ((IDisposable)val)?.Dispose(); - } - } + protected override void OnDraw(SKCanvas canvas, SKRect bounds) + { + DrawPickerButton(canvas, bounds); + } - private void DrawDays(SKCanvas canvas, SKRect bounds) - { - //IL_0079: Unknown result type (might be due to invalid IL or missing references) - //IL_0080: Expected O, but got Unknown - //IL_0082: Unknown result type (might be due to invalid IL or missing references) - //IL_0087: Unknown result type (might be due to invalid IL or missing references) - //IL_0090: Expected O, but got Unknown - //IL_0090: Unknown result type (might be due to invalid IL or missing references) - //IL_0095: Unknown result type (might be due to invalid IL or missing references) - //IL_009c: Unknown result type (might be due to invalid IL or missing references) - //IL_00a5: Expected O, but got Unknown - //IL_0190: Unknown result type (might be due to invalid IL or missing references) - //IL_01d8: Unknown result type (might be due to invalid IL or missing references) - //IL_0231: Unknown result type (might be due to invalid IL or missing references) - //IL_0246: Unknown result type (might be due to invalid IL or missing references) - //IL_022a: Unknown result type (might be due to invalid IL or missing references) - //IL_0222: Unknown result type (might be due to invalid IL or missing references) - DateTime dateTime = new DateTime(_displayMonth.Year, _displayMonth.Month, 1); - int num = DateTime.DaysInMonth(_displayMonth.Year, _displayMonth.Month); - int dayOfWeek = (int)dateTime.DayOfWeek; - float num2 = ((SKRect)(ref bounds)).Width / 7f; - float num3 = (((SKRect)(ref bounds)).Height - 10f) / 6f; - SKFont val = new SKFont(SKTypeface.Default, 14f, 1f, 0f); - try - { - SKPaint val2 = new SKPaint(val) - { - IsAntialias = true - }; - try - { - SKPaint val3 = new SKPaint - { - Style = (SKPaintStyle)0, - IsAntialias = true - }; - try - { - DateTime today = DateTime.Today; - SKRect val4 = default(SKRect); - for (int i = 1; i <= num; i++) - { - DateTime dateTime2 = new DateTime(_displayMonth.Year, _displayMonth.Month, i); - int num4 = dayOfWeek + i - 1; - int num5 = num4 / 7; - int num6 = num4 % 7; - ((SKRect)(ref val4))._002Ector(((SKRect)(ref bounds)).Left + (float)num6 * num2 + 2f, ((SKRect)(ref bounds)).Top + (float)num5 * num3 + 2f, ((SKRect)(ref bounds)).Left + (float)(num6 + 1) * num2 - 2f, ((SKRect)(ref bounds)).Top + (float)(num5 + 1) * num3 - 2f); - bool flag = dateTime2.Date == Date.Date; - bool flag2 = dateTime2.Date == today; - bool flag3 = dateTime2 < MinimumDate || dateTime2 > MaximumDate; - if (flag) - { - val3.Color = SelectedDayColor; - canvas.DrawCircle(((SKRect)(ref val4)).MidX, ((SKRect)(ref val4)).MidY, Math.Min(((SKRect)(ref val4)).Width, ((SKRect)(ref val4)).Height) / 2f - 2f, val3); - } - else if (flag2) - { - val3.Color = TodayColor; - canvas.DrawCircle(((SKRect)(ref val4)).MidX, ((SKRect)(ref val4)).MidY, Math.Min(((SKRect)(ref val4)).Width, ((SKRect)(ref val4)).Height) / 2f - 2f, val3); - } - val2.Color = (flag ? SKColors.White : (flag3 ? DisabledDayColor : TextColor)); - string text = i.ToString(); - SKRect val5 = default(SKRect); - val2.MeasureText(text, ref val5); - canvas.DrawText(text, ((SKRect)(ref val4)).MidX - ((SKRect)(ref val5)).MidX, ((SKRect)(ref val4)).MidY - ((SKRect)(ref val5)).MidY, val2); - } - } - finally - { - ((IDisposable)val3)?.Dispose(); - } - } - finally - { - ((IDisposable)val2)?.Dispose(); - } - } - finally - { - ((IDisposable)val)?.Dispose(); - } - } + private void DrawPickerButton(SKCanvas canvas, SKRect bounds) + { + using var bgPaint = new SKPaint + { + Color = IsEnabled ? BackgroundColor : new SKColor(0xF5, 0xF5, 0xF5), + Style = SKPaintStyle.Fill, + IsAntialias = true + }; + canvas.DrawRoundRect(new SKRoundRect(bounds, CornerRadius), bgPaint); - public override void OnPointerPressed(PointerEventArgs e) - { - //IL_0015: Unknown result type (might be due to invalid IL or missing references) - //IL_001a: Unknown result type (might be due to invalid IL or missing references) - //IL_001c: Unknown result type (might be due to invalid IL or missing references) - //IL_001d: Unknown result type (might be due to invalid IL or missing references) - //IL_0022: Unknown result type (might be due to invalid IL or missing references) - if (!base.IsEnabled) - { - return; - } - if (IsOpen) - { - SKRect screenBounds = base.ScreenBounds; - SKRect calendarRect = GetCalendarRect(screenBounds); - SKRect val = default(SKRect); - ((SKRect)(ref val))._002Ector(((SKRect)(ref calendarRect)).Left, ((SKRect)(ref calendarRect)).Top, ((SKRect)(ref calendarRect)).Right, ((SKRect)(ref calendarRect)).Top + 48f); - if (((SKRect)(ref val)).Contains(e.X, e.Y)) - { - if (e.X < ((SKRect)(ref calendarRect)).Left + 40f) - { - _displayMonth = _displayMonth.AddMonths(-1); - Invalidate(); - } - else if (e.X > ((SKRect)(ref calendarRect)).Right - 40f) - { - _displayMonth = _displayMonth.AddMonths(1); - Invalidate(); - } - return; - } - float num = ((SKRect)(ref calendarRect)).Top + 48f + 30f; - SKRect val2 = default(SKRect); - ((SKRect)(ref val2))._002Ector(((SKRect)(ref calendarRect)).Left, num, ((SKRect)(ref calendarRect)).Right, ((SKRect)(ref calendarRect)).Bottom); - if (((SKRect)(ref val2)).Contains(e.X, e.Y)) - { - float num2 = 40f; - float num3 = 38.666668f; - int num4 = (int)((e.X - ((SKRect)(ref calendarRect)).Left) / num2); - int num5 = (int)((int)((e.Y - num) / num3) * 7 + num4 - new DateTime(_displayMonth.Year, _displayMonth.Month, 1).DayOfWeek + 1); - int num6 = DateTime.DaysInMonth(_displayMonth.Year, _displayMonth.Month); - if (num5 >= 1 && num5 <= num6) - { - DateTime dateTime = new DateTime(_displayMonth.Year, _displayMonth.Month, num5); - if (dateTime >= MinimumDate && dateTime <= MaximumDate) - { - Date = dateTime; - IsOpen = false; - } - } - return; - } - if (((SKRect)(ref screenBounds)).Contains(e.X, e.Y)) - { - IsOpen = false; - } - } - else - { - IsOpen = true; - } - Invalidate(); - } + using var borderPaint = new SKPaint + { + Color = IsFocused ? SelectedDayColor : BorderColor, + Style = SKPaintStyle.Stroke, + StrokeWidth = IsFocused ? 2 : 1, + IsAntialias = true + }; + canvas.DrawRoundRect(new SKRoundRect(bounds, CornerRadius), borderPaint); - public override void OnKeyDown(KeyEventArgs e) - { - if (!base.IsEnabled) - { - return; - } - switch (e.Key) - { - case Key.Enter: - case Key.Space: - IsOpen = !IsOpen; - e.Handled = true; - break; - case Key.Escape: - if (IsOpen) - { - IsOpen = false; - e.Handled = true; - } - break; - case Key.Left: - Date = Date.AddDays(-1.0); - e.Handled = true; - break; - case Key.Right: - Date = Date.AddDays(1.0); - e.Handled = true; - break; - case Key.Up: - Date = Date.AddDays(-7.0); - e.Handled = true; - break; - case Key.Down: - Date = Date.AddDays(7.0); - e.Handled = true; - break; - } - Invalidate(); - } + using var font = new SKFont(SKTypeface.Default, FontSize); + using var textPaint = new SKPaint(font) + { + Color = IsEnabled ? TextColor : TextColor.WithAlpha(128), + IsAntialias = true + }; - public override void OnFocusLost() - { - base.OnFocusLost(); - if (IsOpen) - { - IsOpen = false; - } - } + var dateText = Date.ToString(Format); + var textBounds = new SKRect(); + textPaint.MeasureText(dateText, ref textBounds); + canvas.DrawText(dateText, bounds.Left + 12, bounds.MidY - textBounds.MidY, textPaint); - protected override SKSize MeasureOverride(SKSize availableSize) - { - //IL_002b: Unknown result type (might be due to invalid IL or missing references) - return new SKSize((((SKSize)(ref availableSize)).Width < float.MaxValue) ? Math.Min(((SKSize)(ref availableSize)).Width, 200f) : 200f, 40f); - } + DrawCalendarIcon(canvas, new SKRect(bounds.Right - 36, bounds.MidY - 10, bounds.Right - 12, bounds.MidY + 10)); + } - protected override bool HitTestPopupArea(float x, float y) - { - //IL_0001: Unknown result type (might be due to invalid IL or missing references) - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - //IL_001d: Unknown result type (might be due to invalid IL or missing references) - //IL_001e: Unknown result type (might be due to invalid IL or missing references) - //IL_0023: Unknown result type (might be due to invalid IL or missing references) - SKRect screenBounds = base.ScreenBounds; - if (((SKRect)(ref screenBounds)).Contains(x, y)) - { - return true; - } - if (_isOpen) - { - SKRect calendarRect = GetCalendarRect(screenBounds); - return ((SKRect)(ref calendarRect)).Contains(x, y); - } - return false; - } + private void DrawCalendarIcon(SKCanvas canvas, SKRect bounds) + { + using var paint = new SKPaint + { + Color = IsEnabled ? TextColor : TextColor.WithAlpha(128), + Style = SKPaintStyle.Stroke, + StrokeWidth = 1.5f, + IsAntialias = true + }; + + var calRect = new SKRect(bounds.Left, bounds.Top + 3, bounds.Right, bounds.Bottom); + canvas.DrawRoundRect(new SKRoundRect(calRect, 2), paint); + canvas.DrawLine(bounds.Left + 5, bounds.Top, bounds.Left + 5, bounds.Top + 5, paint); + canvas.DrawLine(bounds.Right - 5, bounds.Top, bounds.Right - 5, bounds.Top + 5, paint); + canvas.DrawLine(bounds.Left, bounds.Top + 8, bounds.Right, bounds.Top + 8, paint); + + paint.Style = SKPaintStyle.Fill; + for (int row = 0; row < 2; row++) + for (int col = 0; col < 3; col++) + canvas.DrawCircle(bounds.Left + 4 + col * 6, bounds.Top + 12 + row * 4, 1, paint); + } + + private void DrawCalendar(SKCanvas canvas, SKRect bounds) + { + var calendarRect = GetCalendarRect(bounds); + + using var shadowPaint = new SKPaint + { + Color = new SKColor(0, 0, 0, 40), + MaskFilter = SKMaskFilter.CreateBlur(SKBlurStyle.Normal, 4), + Style = SKPaintStyle.Fill + }; + canvas.DrawRoundRect(new SKRoundRect(new SKRect(calendarRect.Left + 2, calendarRect.Top + 2, calendarRect.Right + 2, calendarRect.Bottom + 2), CornerRadius), shadowPaint); + + using var bgPaint = new SKPaint { Color = CalendarBackgroundColor, Style = SKPaintStyle.Fill, IsAntialias = true }; + canvas.DrawRoundRect(new SKRoundRect(calendarRect, CornerRadius), bgPaint); + + using var borderPaint = new SKPaint { Color = BorderColor, Style = SKPaintStyle.Stroke, StrokeWidth = 1, IsAntialias = true }; + canvas.DrawRoundRect(new SKRoundRect(calendarRect, CornerRadius), borderPaint); + + DrawCalendarHeader(canvas, new SKRect(calendarRect.Left, calendarRect.Top, calendarRect.Right, calendarRect.Top + HeaderHeight)); + DrawWeekdayHeaders(canvas, new SKRect(calendarRect.Left, calendarRect.Top + HeaderHeight, calendarRect.Right, calendarRect.Top + HeaderHeight + 30)); + DrawDays(canvas, new SKRect(calendarRect.Left, calendarRect.Top + HeaderHeight + 30, calendarRect.Right, calendarRect.Bottom)); + } + + private void DrawCalendarHeader(SKCanvas canvas, SKRect bounds) + { + using var headerPaint = new SKPaint { Color = HeaderColor, Style = SKPaintStyle.Fill }; + canvas.Save(); + canvas.ClipRoundRect(new SKRoundRect(new SKRect(bounds.Left, bounds.Top, bounds.Right, bounds.Top + CornerRadius * 2), CornerRadius)); + canvas.DrawRect(bounds, headerPaint); + canvas.Restore(); + canvas.DrawRect(new SKRect(bounds.Left, bounds.Top + CornerRadius, bounds.Right, bounds.Bottom), headerPaint); + + using var font = new SKFont(SKTypeface.Default, 16); + using var textPaint = new SKPaint(font) { Color = SKColors.White, IsAntialias = true }; + var monthYear = _displayMonth.ToString("MMMM yyyy"); + var textBounds = new SKRect(); + textPaint.MeasureText(monthYear, ref textBounds); + canvas.DrawText(monthYear, bounds.MidX - textBounds.MidX, bounds.MidY - textBounds.MidY, textPaint); + + using var arrowPaint = new SKPaint { Color = SKColors.White, Style = SKPaintStyle.Stroke, StrokeWidth = 2, IsAntialias = true, StrokeCap = SKStrokeCap.Round }; + using var leftPath = new SKPath(); + leftPath.MoveTo(bounds.Left + 26, bounds.MidY - 6); + leftPath.LineTo(bounds.Left + 20, bounds.MidY); + leftPath.LineTo(bounds.Left + 26, bounds.MidY + 6); + canvas.DrawPath(leftPath, arrowPaint); + + using var rightPath = new SKPath(); + rightPath.MoveTo(bounds.Right - 26, bounds.MidY - 6); + rightPath.LineTo(bounds.Right - 20, bounds.MidY); + rightPath.LineTo(bounds.Right - 26, bounds.MidY + 6); + canvas.DrawPath(rightPath, arrowPaint); + } + + private void DrawWeekdayHeaders(SKCanvas canvas, SKRect bounds) + { + var dayNames = new[] { "Su", "Mo", "Tu", "We", "Th", "Fr", "Sa" }; + var cellWidth = bounds.Width / 7; + using var font = new SKFont(SKTypeface.Default, 12); + using var paint = new SKPaint(font) { Color = new SKColor(0x80, 0x80, 0x80), IsAntialias = true }; + for (int i = 0; i < 7; i++) + { + var textBounds = new SKRect(); + paint.MeasureText(dayNames[i], ref textBounds); + canvas.DrawText(dayNames[i], bounds.Left + i * cellWidth + cellWidth / 2 - textBounds.MidX, bounds.MidY - textBounds.MidY, paint); + } + } + + private void DrawDays(SKCanvas canvas, SKRect bounds) + { + var firstDay = new DateTime(_displayMonth.Year, _displayMonth.Month, 1); + var daysInMonth = DateTime.DaysInMonth(_displayMonth.Year, _displayMonth.Month); + var startDayOfWeek = (int)firstDay.DayOfWeek; + var cellWidth = bounds.Width / 7; + var cellHeight = (bounds.Height - 10) / 6; + using var font = new SKFont(SKTypeface.Default, 14); + using var textPaint = new SKPaint(font) { IsAntialias = true }; + using var bgPaint = new SKPaint { Style = SKPaintStyle.Fill, IsAntialias = true }; + var today = DateTime.Today; + + for (int day = 1; day <= daysInMonth; day++) + { + var dayDate = new DateTime(_displayMonth.Year, _displayMonth.Month, day); + var cellIndex = startDayOfWeek + day - 1; + var row = cellIndex / 7; + var col = cellIndex % 7; + var cellRect = new SKRect(bounds.Left + col * cellWidth + 2, bounds.Top + row * cellHeight + 2, bounds.Left + (col + 1) * cellWidth - 2, bounds.Top + (row + 1) * cellHeight - 2); + + var isSelected = dayDate.Date == Date.Date; + var isToday = dayDate.Date == today; + var isDisabled = dayDate < MinimumDate || dayDate > MaximumDate; + + if (isSelected) + { + bgPaint.Color = SelectedDayColor; + canvas.DrawCircle(cellRect.MidX, cellRect.MidY, Math.Min(cellRect.Width, cellRect.Height) / 2 - 2, bgPaint); + } + else if (isToday) + { + bgPaint.Color = TodayColor; + canvas.DrawCircle(cellRect.MidX, cellRect.MidY, Math.Min(cellRect.Width, cellRect.Height) / 2 - 2, bgPaint); + } + + textPaint.Color = isSelected ? SKColors.White : isDisabled ? DisabledDayColor : TextColor; + var dayText = day.ToString(); + var textBounds = new SKRect(); + textPaint.MeasureText(dayText, ref textBounds); + canvas.DrawText(dayText, cellRect.MidX - textBounds.MidX, cellRect.MidY - textBounds.MidY, textPaint); + } + } + + public override void OnPointerPressed(PointerEventArgs e) + { + if (!IsEnabled) return; + + if (IsOpen) + { + // Use ScreenBounds for popup coordinate calculations (accounts for scroll offset) + var screenBounds = ScreenBounds; + var calendarRect = GetCalendarRect(screenBounds); + + // Check if click is in header area (navigation arrows) + var headerRect = new SKRect(calendarRect.Left, calendarRect.Top, calendarRect.Right, calendarRect.Top + HeaderHeight); + if (headerRect.Contains(e.X, e.Y)) + { + if (e.X < calendarRect.Left + 40) { _displayMonth = _displayMonth.AddMonths(-1); Invalidate(); return; } + if (e.X > calendarRect.Right - 40) { _displayMonth = _displayMonth.AddMonths(1); Invalidate(); return; } + return; + } + + // Check if click is in days area + var daysTop = calendarRect.Top + HeaderHeight + 30; + var daysRect = new SKRect(calendarRect.Left, daysTop, calendarRect.Right, calendarRect.Bottom); + if (daysRect.Contains(e.X, e.Y)) + { + var cellWidth = CalendarWidth / 7; + var cellHeight = (CalendarHeight - HeaderHeight - 40) / 6; + var col = (int)((e.X - calendarRect.Left) / cellWidth); + var row = (int)((e.Y - daysTop) / cellHeight); + var firstDay = new DateTime(_displayMonth.Year, _displayMonth.Month, 1); + var dayIndex = row * 7 + col - (int)firstDay.DayOfWeek + 1; + var daysInMonth = DateTime.DaysInMonth(_displayMonth.Year, _displayMonth.Month); + if (dayIndex >= 1 && dayIndex <= daysInMonth) + { + var selectedDate = new DateTime(_displayMonth.Year, _displayMonth.Month, dayIndex); + if (selectedDate >= MinimumDate && selectedDate <= MaximumDate) + { + Date = selectedDate; + IsOpen = false; + } + } + return; + } + + // Click is outside calendar - check if it's on the picker itself + if (screenBounds.Contains(e.X, e.Y)) + { + IsOpen = false; + } + } + else IsOpen = true; + Invalidate(); + } + + public override void OnKeyDown(KeyEventArgs e) + { + if (!IsEnabled) return; + switch (e.Key) + { + case Key.Enter: case Key.Space: IsOpen = !IsOpen; e.Handled = true; break; + case Key.Escape: if (IsOpen) { IsOpen = false; e.Handled = true; } break; + case Key.Left: Date = Date.AddDays(-1); e.Handled = true; break; + case Key.Right: Date = Date.AddDays(1); e.Handled = true; break; + case Key.Up: Date = Date.AddDays(-7); e.Handled = true; break; + case Key.Down: Date = Date.AddDays(7); e.Handled = true; break; + } + Invalidate(); + } + + public override void OnFocusLost() + { + base.OnFocusLost(); + // Close popup when focus is lost (clicking outside) + if (IsOpen) + { + IsOpen = false; + } + } + + protected override SKSize MeasureOverride(SKSize availableSize) + { + return new SKSize(availableSize.Width < float.MaxValue ? Math.Min(availableSize.Width, 200) : 200, 40); + } + + /// + /// Override to include calendar popup area in hit testing. + /// + protected override bool HitTestPopupArea(float x, float y) + { + // Use ScreenBounds for hit testing (accounts for scroll offset) + var screenBounds = ScreenBounds; + + // Always include the picker button itself + if (screenBounds.Contains(x, y)) + return true; + + // When open, also include the calendar area (with edge detection) + if (_isOpen) + { + var calendarRect = GetCalendarRect(screenBounds); + return calendarRect.Contains(x, y); + } + + return false; + } } diff --git a/Views/SkiaEditor.cs b/Views/SkiaEditor.cs index 2498fc7..daa04c1 100644 --- a/Views/SkiaEditor.cs +++ b/Views/SkiaEditor.cs @@ -1,1105 +1,1072 @@ -using System; -using System.Collections.Generic; -using Microsoft.Maui.Controls; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using SkiaSharp; namespace Microsoft.Maui.Platform; +/// +/// Skia-rendered multiline text editor control with full XAML styling support. +/// public class SkiaEditor : SkiaView { - public static readonly BindableProperty TextProperty = BindableProperty.Create("Text", typeof(string), typeof(SkiaEditor), (object)"", (BindingMode)1, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaEditor)(object)b).OnTextPropertyChanged((string)o, (string)n); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty PlaceholderProperty = BindableProperty.Create("Placeholder", typeof(string), typeof(SkiaEditor), (object)"", (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaEditor)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty TextColorProperty = BindableProperty.Create("TextColor", typeof(SKColor), typeof(SkiaEditor), (object)SKColors.Black, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaEditor)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty PlaceholderColorProperty = BindableProperty.Create("PlaceholderColor", typeof(SKColor), typeof(SkiaEditor), (object)new SKColor((byte)128, (byte)128, (byte)128), (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaEditor)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty BorderColorProperty = BindableProperty.Create("BorderColor", typeof(SKColor), typeof(SkiaEditor), (object)new SKColor((byte)189, (byte)189, (byte)189), (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaEditor)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty SelectionColorProperty = BindableProperty.Create("SelectionColor", typeof(SKColor), typeof(SkiaEditor), (object)new SKColor((byte)33, (byte)150, (byte)243, (byte)96), (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaEditor)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty CursorColorProperty = BindableProperty.Create("CursorColor", typeof(SKColor), typeof(SkiaEditor), (object)new SKColor((byte)33, (byte)150, (byte)243), (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaEditor)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty FontFamilyProperty = BindableProperty.Create("FontFamily", typeof(string), typeof(SkiaEditor), (object)"Sans", (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaEditor)(object)b).InvalidateMeasure(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty FontSizeProperty = BindableProperty.Create("FontSize", typeof(float), typeof(SkiaEditor), (object)14f, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaEditor)(object)b).InvalidateMeasure(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty LineHeightProperty = BindableProperty.Create("LineHeight", typeof(float), typeof(SkiaEditor), (object)1.4f, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaEditor)(object)b).InvalidateMeasure(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty CornerRadiusProperty = BindableProperty.Create("CornerRadius", typeof(float), typeof(SkiaEditor), (object)4f, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaEditor)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty PaddingProperty = BindableProperty.Create("Padding", typeof(float), typeof(SkiaEditor), (object)12f, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaEditor)(object)b).InvalidateMeasure(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty IsReadOnlyProperty = BindableProperty.Create("IsReadOnly", typeof(bool), typeof(SkiaEditor), (object)false, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaEditor)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty MaxLengthProperty = BindableProperty.Create("MaxLength", typeof(int), typeof(SkiaEditor), (object)(-1), (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)null, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty AutoSizeProperty = BindableProperty.Create("AutoSize", typeof(bool), typeof(SkiaEditor), (object)false, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaEditor)(object)b).InvalidateMeasure(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - private int _cursorPosition; - - private int _selectionStart = -1; - - private int _selectionLength; - - private float _scrollOffsetY; - - private bool _cursorVisible = true; - - private DateTime _lastCursorBlink = DateTime.Now; - - private List _lines = new List { "" }; - - private float _wrapWidth; - - private bool _isSelecting; - - private DateTime _lastClickTime = DateTime.MinValue; - - private float _lastClickX; - - private float _lastClickY; - - private const double DoubleClickThresholdMs = 400.0; - - public string Text - { - get - { - return (string)((BindableObject)this).GetValue(TextProperty); - } - set - { - ((BindableObject)this).SetValue(TextProperty, (object)value); - } - } - - public string Placeholder - { - get - { - return (string)((BindableObject)this).GetValue(PlaceholderProperty); - } - set - { - ((BindableObject)this).SetValue(PlaceholderProperty, (object)value); - } - } - - public SKColor TextColor - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKColor)((BindableObject)this).GetValue(TextColorProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(TextColorProperty, (object)value); - } - } - - public SKColor PlaceholderColor - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKColor)((BindableObject)this).GetValue(PlaceholderColorProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(PlaceholderColorProperty, (object)value); - } - } - - public SKColor BorderColor - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKColor)((BindableObject)this).GetValue(BorderColorProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(BorderColorProperty, (object)value); - } - } - - public SKColor SelectionColor - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKColor)((BindableObject)this).GetValue(SelectionColorProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(SelectionColorProperty, (object)value); - } - } - - public SKColor CursorColor - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKColor)((BindableObject)this).GetValue(CursorColorProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(CursorColorProperty, (object)value); - } - } - - public string FontFamily - { - get - { - return (string)((BindableObject)this).GetValue(FontFamilyProperty); - } - set - { - ((BindableObject)this).SetValue(FontFamilyProperty, (object)value); - } - } - - public float FontSize - { - get - { - return (float)((BindableObject)this).GetValue(FontSizeProperty); - } - set - { - ((BindableObject)this).SetValue(FontSizeProperty, (object)value); - } - } - - public float LineHeight - { - get - { - return (float)((BindableObject)this).GetValue(LineHeightProperty); - } - set - { - ((BindableObject)this).SetValue(LineHeightProperty, (object)value); - } - } - - public float CornerRadius - { - get - { - return (float)((BindableObject)this).GetValue(CornerRadiusProperty); - } - set - { - ((BindableObject)this).SetValue(CornerRadiusProperty, (object)value); - } - } - - public float Padding - { - get - { - return (float)((BindableObject)this).GetValue(PaddingProperty); - } - set - { - ((BindableObject)this).SetValue(PaddingProperty, (object)value); - } - } - - public bool IsReadOnly - { - get - { - return (bool)((BindableObject)this).GetValue(IsReadOnlyProperty); - } - set - { - ((BindableObject)this).SetValue(IsReadOnlyProperty, (object)value); - } - } - - public int MaxLength - { - get - { - return (int)((BindableObject)this).GetValue(MaxLengthProperty); - } - set - { - ((BindableObject)this).SetValue(MaxLengthProperty, (object)value); - } - } - - public bool AutoSize - { - get - { - return (bool)((BindableObject)this).GetValue(AutoSizeProperty); - } - set - { - ((BindableObject)this).SetValue(AutoSizeProperty, (object)value); - } - } - - public int CursorPosition - { - get - { - return _cursorPosition; - } - set - { - _cursorPosition = Math.Clamp(value, 0, Text.Length); - EnsureCursorVisible(); - Invalidate(); - } - } - - public event EventHandler? TextChanged; - - public event EventHandler? Completed; - - public SkiaEditor() - { - base.IsFocusable = true; - } - - private void OnTextPropertyChanged(string oldText, string newText) - { - string text = newText ?? ""; - if (MaxLength > 0 && text.Length > MaxLength) - { - text = text.Substring(0, MaxLength); - ((BindableObject)this).SetValue(TextProperty, (object)text); - return; - } - UpdateLines(); - _cursorPosition = Math.Min(_cursorPosition, text.Length); - _scrollOffsetY = 0f; - _selectionLength = 0; - this.TextChanged?.Invoke(this, EventArgs.Empty); - Invalidate(); - } - - private void UpdateLines() - { - //IL_0049: Unknown result type (might be due to invalid IL or missing references) - //IL_004f: Expected O, but got Unknown - _lines.Clear(); - string text = Text ?? ""; - if (string.IsNullOrEmpty(text)) - { - _lines.Add(""); - return; - } - SKFont val = new SKFont(SKTypeface.Default, FontSize, 1f, 0f); - try - { - string[] array = text.Split('\n'); - foreach (string text2 in array) - { - if (string.IsNullOrEmpty(text2)) - { - _lines.Add(""); - } - else if (_wrapWidth > 0f) - { - WrapParagraph(text2, val, _wrapWidth); - } - else - { - _lines.Add(text2); - } - } - if (_lines.Count == 0) - { - _lines.Add(""); - } - } - finally - { - ((IDisposable)val)?.Dispose(); - } - } - - private void WrapParagraph(string paragraph, SKFont font, float maxWidth) - { - string[] array = paragraph.Split(' '); - string text = ""; - string[] array2 = array; - foreach (string text2 in array2) - { - string text3 = (string.IsNullOrEmpty(text) ? text2 : (text + " " + text2)); - if (MeasureText(text3, font) > maxWidth && !string.IsNullOrEmpty(text)) - { - _lines.Add(text); - text = text2; - } - else - { - text = text3; - } - } - if (!string.IsNullOrEmpty(text)) - { - _lines.Add(text); - } - } - - private (int line, int column) GetLineColumn(int position) - { - int num = 0; - for (int i = 0; i < _lines.Count; i++) - { - int length = _lines[i].Length; - if (num + length >= position || i == _lines.Count - 1) - { - return (line: i, column: position - num); - } - num += length + 1; - } - int item = _lines.Count - 1; - List lines = _lines; - return (line: item, column: lines[lines.Count - 1].Length); - } - - private int GetPosition(int line, int column) - { - int num = 0; - for (int i = 0; i < line && i < _lines.Count; i++) - { - num += _lines[i].Length + 1; - } - if (line < _lines.Count) - { - num += Math.Min(column, _lines[line].Length); - } - return Math.Min(num, Text.Length); - } - - protected override void OnDraw(SKCanvas canvas, SKRect bounds) - { - //IL_007c: Unknown result type (might be due to invalid IL or missing references) - //IL_0081: Unknown result type (might be due to invalid IL or missing references) - //IL_00a1: Unknown result type (might be due to invalid IL or missing references) - //IL_0099: Unknown result type (might be due to invalid IL or missing references) - //IL_00ab: Unknown result type (might be due to invalid IL or missing references) - //IL_00b2: Unknown result type (might be due to invalid IL or missing references) - //IL_00ba: Expected O, but got Unknown - //IL_00bb: Unknown result type (might be due to invalid IL or missing references) - //IL_00c2: Unknown result type (might be due to invalid IL or missing references) - //IL_00cd: Expected O, but got Unknown - //IL_00cd: Unknown result type (might be due to invalid IL or missing references) - //IL_00d2: Unknown result type (might be due to invalid IL or missing references) - //IL_00e4: Unknown result type (might be due to invalid IL or missing references) - //IL_00dc: Unknown result type (might be due to invalid IL or missing references) - //IL_00ee: Unknown result type (might be due to invalid IL or missing references) - //IL_00f5: Unknown result type (might be due to invalid IL or missing references) - //IL_0108: Unknown result type (might be due to invalid IL or missing references) - //IL_0110: Expected O, but got Unknown - //IL_0111: Unknown result type (might be due to invalid IL or missing references) - //IL_0118: Unknown result type (might be due to invalid IL or missing references) - //IL_0123: Expected O, but got Unknown - //IL_0138: Unknown result type (might be due to invalid IL or missing references) - //IL_013e: Expected O, but got Unknown - //IL_0185: Unknown result type (might be due to invalid IL or missing references) - //IL_018a: Unknown result type (might be due to invalid IL or missing references) - //IL_0194: Unknown result type (might be due to invalid IL or missing references) - //IL_0207: Unknown result type (might be due to invalid IL or missing references) - //IL_020c: Unknown result type (might be due to invalid IL or missing references) - //IL_022c: Unknown result type (might be due to invalid IL or missing references) - //IL_0216: Unknown result type (might be due to invalid IL or missing references) - //IL_021b: Unknown result type (might be due to invalid IL or missing references) - //IL_0224: Unknown result type (might be due to invalid IL or missing references) - //IL_01b8: Unknown result type (might be due to invalid IL or missing references) - //IL_01bd: Unknown result type (might be due to invalid IL or missing references) - //IL_01bf: Unknown result type (might be due to invalid IL or missing references) - //IL_01c9: Unknown result type (might be due to invalid IL or missing references) - //IL_01d2: Expected O, but got Unknown - //IL_0236: Unknown result type (might be due to invalid IL or missing references) - //IL_023f: Expected O, but got Unknown - //IL_023f: Unknown result type (might be due to invalid IL or missing references) - //IL_0244: Unknown result type (might be due to invalid IL or missing references) - //IL_0246: Unknown result type (might be due to invalid IL or missing references) - //IL_0250: Unknown result type (might be due to invalid IL or missing references) - //IL_0259: Expected O, but got Unknown - //IL_04ab: Unknown result type (might be due to invalid IL or missing references) - //IL_03d8: Unknown result type (might be due to invalid IL or missing references) - //IL_03dd: Unknown result type (might be due to invalid IL or missing references) - //IL_03df: Unknown result type (might be due to invalid IL or missing references) - //IL_03e9: Unknown result type (might be due to invalid IL or missing references) - //IL_03f0: Unknown result type (might be due to invalid IL or missing references) - //IL_03fb: Unknown result type (might be due to invalid IL or missing references) - //IL_0404: Expected O, but got Unknown - //IL_0365: Unknown result type (might be due to invalid IL or missing references) - float num = ((SKRect)(ref bounds)).Width - Padding * 2f; - if (Math.Abs(num - _wrapWidth) > 1f) - { - _wrapWidth = num; - UpdateLines(); - } - if (base.IsFocused && (DateTime.Now - _lastCursorBlink).TotalMilliseconds > 500.0) - { - _cursorVisible = !_cursorVisible; - _lastCursorBlink = DateTime.Now; - } - SKPaint val = new SKPaint - { - Color = (SKColor)(base.IsEnabled ? base.BackgroundColor : new SKColor((byte)245, (byte)245, (byte)245)), - Style = (SKPaintStyle)0, - IsAntialias = true - }; - try - { - canvas.DrawRoundRect(new SKRoundRect(bounds, CornerRadius), val); - SKPaint val2 = new SKPaint - { - Color = (base.IsFocused ? CursorColor : BorderColor), - Style = (SKPaintStyle)1, - StrokeWidth = ((!base.IsFocused) ? 1 : 2), - IsAntialias = true - }; - try - { - canvas.DrawRoundRect(new SKRoundRect(bounds, CornerRadius), val2); - SKFont val3 = new SKFont(SKTypeface.Default, FontSize, 1f, 0f); - try - { - float num2 = FontSize * LineHeight; - SKRect val4 = new SKRect(((SKRect)(ref bounds)).Left + Padding, ((SKRect)(ref bounds)).Top + Padding, ((SKRect)(ref bounds)).Right - Padding, ((SKRect)(ref bounds)).Bottom - Padding); - canvas.Save(); - canvas.ClipRect(val4, (SKClipOperation)1, false); - if (string.IsNullOrEmpty(Text) && !string.IsNullOrEmpty(Placeholder)) - { - SKPaint val5 = new SKPaint(val3) - { - Color = PlaceholderColor, - IsAntialias = true - }; - try - { - canvas.DrawText(Placeholder, ((SKRect)(ref val4)).Left, ((SKRect)(ref val4)).Top + FontSize, val5); - } - finally - { - ((IDisposable)val5)?.Dispose(); - } - } - else - { - SKPaint val6 = new SKPaint(val3); - SKColor color; - if (!base.IsEnabled) - { - SKColor textColor = TextColor; - color = ((SKColor)(ref textColor)).WithAlpha((byte)128); - } - else - { - color = TextColor; - } - val6.Color = color; - val6.IsAntialias = true; - SKPaint val7 = val6; - try - { - SKPaint val8 = new SKPaint - { - Color = SelectionColor, - Style = (SKPaintStyle)0 - }; - try - { - float num3 = ((SKRect)(ref val4)).Top + FontSize; - int num4 = 0; - for (int i = 0; i < _lines.Count; i++) - { - string text = _lines[i]; - float left = ((SKRect)(ref val4)).Left; - if (_selectionStart >= 0 && _selectionLength != 0) - { - int num5 = ((_selectionLength > 0) ? _selectionStart : (_selectionStart + _selectionLength)); - int num6 = ((_selectionLength > 0) ? (_selectionStart + _selectionLength) : _selectionStart); - int num7 = num4; - int num8 = num4 + text.Length; - if (num6 > num7 && num5 < num8) - { - int length = Math.Max(0, num5 - num7); - int length2 = Math.Min(text.Length, num6 - num7); - float num9 = left + MeasureText(text.Substring(0, length), val3); - float num10 = left + MeasureText(text.Substring(0, length2), val3); - canvas.DrawRect(new SKRect(num9, num3 - FontSize, num10, num3 + num2 - FontSize), val8); - } - } - canvas.DrawText(text, left, num3, val7); - if (base.IsFocused && _cursorVisible) - { - var (num11, val9) = GetLineColumn(_cursorPosition); - if (num11 == i) - { - float num12 = left + MeasureText(text.Substring(0, Math.Min(val9, text.Length)), val3); - SKPaint val10 = new SKPaint - { - Color = CursorColor, - Style = (SKPaintStyle)1, - StrokeWidth = 2f, - IsAntialias = true - }; - try - { - canvas.DrawLine(num12, num3 - FontSize + 2f, num12, num3 + 2f, val10); - } - finally - { - ((IDisposable)val10)?.Dispose(); - } - } - } - num3 += num2; - num4 += text.Length + 1; - } - } - finally - { - ((IDisposable)val8)?.Dispose(); - } - } - finally - { - ((IDisposable)val7)?.Dispose(); - } - } - canvas.Restore(); - float num13 = (float)_lines.Count * FontSize * LineHeight; - if (num13 > ((SKRect)(ref val4)).Height) - { - DrawScrollbar(canvas, bounds, ((SKRect)(ref val4)).Height, num13); - } - } - finally - { - ((IDisposable)val3)?.Dispose(); - } - } - finally - { - ((IDisposable)val2)?.Dispose(); - } - } - finally - { - ((IDisposable)val)?.Dispose(); - } - } - - private float MeasureText(string text, SKFont font) - { - //IL_000f: Unknown result type (might be due to invalid IL or missing references) - //IL_0015: Expected O, but got Unknown - if (string.IsNullOrEmpty(text)) - { - return 0f; - } - SKPaint val = new SKPaint(font); - try - { - return val.MeasureText(text); - } - finally - { - ((IDisposable)val)?.Dispose(); - } - } - - private void DrawScrollbar(SKCanvas canvas, SKRect bounds, float viewHeight, float contentHeight) - { - //IL_003a: Unknown result type (might be due to invalid IL or missing references) - //IL_003f: Unknown result type (might be due to invalid IL or missing references) - //IL_0045: Unknown result type (might be due to invalid IL or missing references) - //IL_004f: Unknown result type (might be due to invalid IL or missing references) - //IL_0056: Unknown result type (might be due to invalid IL or missing references) - //IL_005f: Expected O, but got Unknown - //IL_0078: Unknown result type (might be due to invalid IL or missing references) - //IL_0084: Unknown result type (might be due to invalid IL or missing references) - //IL_0090: Expected O, but got Unknown - float num = 6f; - float num2 = 2f; - float num3 = Math.Max(20f, viewHeight * (viewHeight / contentHeight)); - float num4 = ((SKRect)(ref bounds)).Top + Padding + _scrollOffsetY / contentHeight * (viewHeight - num3); - SKPaint val = new SKPaint - { - Color = new SKColor((byte)0, (byte)0, (byte)0, (byte)60), - Style = (SKPaintStyle)0, - IsAntialias = true - }; - try - { - canvas.DrawRoundRect(new SKRoundRect(new SKRect(((SKRect)(ref bounds)).Right - num - num2, num4, ((SKRect)(ref bounds)).Right - num2, num4 + num3), num / 2f), val); - } - finally - { - ((IDisposable)val)?.Dispose(); - } - } - - private void EnsureCursorVisible() - { - //IL_0028: Unknown result type (might be due to invalid IL or missing references) - //IL_002d: Unknown result type (might be due to invalid IL or missing references) - int item = GetLineColumn(_cursorPosition).line; - float num = FontSize * LineHeight; - float num2 = (float)item * num; - SKRect bounds = base.Bounds; - float num3 = ((SKRect)(ref bounds)).Height - Padding * 2f; - if (num2 < _scrollOffsetY) - { - _scrollOffsetY = num2; - } - else if (num2 + num > _scrollOffsetY + num3) - { - _scrollOffsetY = num2 + num - num3; - } - } - - public override void OnPointerPressed(PointerEventArgs e) - { - //IL_0011: Unknown result type (might be due to invalid IL or missing references) - //IL_0016: Unknown result type (might be due to invalid IL or missing references) - //IL_0083: Unknown result type (might be due to invalid IL or missing references) - //IL_008a: Expected O, but got Unknown - if (!base.IsEnabled) - { - return; - } - base.IsFocused = true; - SKRect screenBounds = base.ScreenBounds; - float num = e.X - ((SKRect)(ref screenBounds)).Left - Padding; - float num2 = e.Y - ((SKRect)(ref screenBounds)).Top - Padding + _scrollOffsetY; - float num3 = FontSize * LineHeight; - int num4 = Math.Clamp((int)(num2 / num3), 0, _lines.Count - 1); - SKFont val = new SKFont(SKTypeface.Default, FontSize, 1f, 0f); - try - { - string text = _lines[num4]; - int column = 0; - for (int i = 0; i <= text.Length; i++) - { - if (MeasureText(text.Substring(0, i), val) > num) - { - column = ((i > 0) ? (i - 1) : 0); - break; - } - column = i; - } - _cursorPosition = GetPosition(num4, column); - DateTime utcNow = DateTime.UtcNow; - double totalMilliseconds = (utcNow - _lastClickTime).TotalMilliseconds; - double num5 = Math.Sqrt(Math.Pow(e.X - _lastClickX, 2.0) + Math.Pow(e.Y - _lastClickY, 2.0)); - if (totalMilliseconds < 400.0 && num5 < 10.0) - { - SelectWordAtCursor(); - _lastClickTime = DateTime.MinValue; - _isSelecting = false; - } - else - { - _selectionStart = _cursorPosition; - _selectionLength = 0; - _isSelecting = true; - _lastClickTime = utcNow; - _lastClickX = e.X; - _lastClickY = e.Y; - } - _cursorVisible = true; - _lastCursorBlink = DateTime.Now; - Invalidate(); - } - finally - { - ((IDisposable)val)?.Dispose(); - } - } - - public override void OnPointerMoved(PointerEventArgs e) - { - //IL_0012: Unknown result type (might be due to invalid IL or missing references) - //IL_0017: Unknown result type (might be due to invalid IL or missing references) - //IL_0084: Unknown result type (might be due to invalid IL or missing references) - //IL_008b: Expected O, but got Unknown - if (!base.IsEnabled || !_isSelecting) - { - return; - } - SKRect screenBounds = base.ScreenBounds; - float num = e.X - ((SKRect)(ref screenBounds)).Left - Padding; - float num2 = e.Y - ((SKRect)(ref screenBounds)).Top - Padding + _scrollOffsetY; - float num3 = FontSize * LineHeight; - int num4 = Math.Clamp((int)(num2 / num3), 0, _lines.Count - 1); - SKFont val = new SKFont(SKTypeface.Default, FontSize, 1f, 0f); - try - { - string text = _lines[num4]; - int column = 0; - for (int i = 0; i <= text.Length; i++) - { - if (MeasureText(text.Substring(0, i), val) > num) - { - column = ((i > 0) ? (i - 1) : 0); - break; - } - column = i; - } - int position = GetPosition(num4, column); - if (position != _cursorPosition) - { - _cursorPosition = position; - _selectionLength = _cursorPosition - _selectionStart; - _cursorVisible = true; - _lastCursorBlink = DateTime.Now; - Invalidate(); - } - } - finally - { - ((IDisposable)val)?.Dispose(); - } - } - - public override void OnPointerReleased(PointerEventArgs e) - { - _isSelecting = false; - } - - public override void OnKeyDown(KeyEventArgs e) - { - if (!base.IsEnabled) - { - return; - } - (int line, int column) lineColumn = GetLineColumn(_cursorPosition); - int item = lineColumn.line; - int item2 = lineColumn.column; - _cursorVisible = true; - _lastCursorBlink = DateTime.Now; - switch (e.Key) - { - case Key.Left: - if (_cursorPosition > 0) - { - _cursorPosition--; - EnsureCursorVisible(); - } - e.Handled = true; - break; - case Key.Right: - if (_cursorPosition < Text.Length) - { - _cursorPosition++; - EnsureCursorVisible(); - } - e.Handled = true; - break; - case Key.Up: - if (item > 0) - { - _cursorPosition = GetPosition(item - 1, item2); - EnsureCursorVisible(); - } - e.Handled = true; - break; - case Key.Down: - if (item < _lines.Count - 1) - { - _cursorPosition = GetPosition(item + 1, item2); - EnsureCursorVisible(); - } - e.Handled = true; - break; - case Key.Home: - _cursorPosition = GetPosition(item, 0); - EnsureCursorVisible(); - e.Handled = true; - break; - case Key.End: - _cursorPosition = GetPosition(item, _lines[item].Length); - EnsureCursorVisible(); - e.Handled = true; - break; - case Key.Enter: - if (!IsReadOnly) - { - InsertText("\n"); - } - e.Handled = true; - break; - case Key.Backspace: - if (!IsReadOnly) - { - if (_selectionLength != 0) - { - DeleteSelection(); - } - else if (_cursorPosition > 0) - { - Text = Text.Remove(_cursorPosition - 1, 1); - _cursorPosition--; - } - EnsureCursorVisible(); - } - e.Handled = true; - break; - case Key.Delete: - if (!IsReadOnly) - { - if (_selectionLength != 0) - { - DeleteSelection(); - } - else if (_cursorPosition < Text.Length) - { - Text = Text.Remove(_cursorPosition, 1); - } - } - e.Handled = true; - break; - case Key.Tab: - if (!IsReadOnly) - { - InsertText(" "); - } - e.Handled = true; - break; - case Key.A: - if (e.Modifiers.HasFlag(KeyModifiers.Control)) - { - SelectAll(); - e.Handled = true; - } - break; - case Key.C: - if (e.Modifiers.HasFlag(KeyModifiers.Control)) - { - CopyToClipboard(); - e.Handled = true; - } - break; - case Key.V: - if (e.Modifiers.HasFlag(KeyModifiers.Control) && !IsReadOnly) - { - PasteFromClipboard(); - e.Handled = true; - } - break; - case Key.X: - if (e.Modifiers.HasFlag(KeyModifiers.Control) && !IsReadOnly) - { - CutToClipboard(); - e.Handled = true; - } - break; - } - Invalidate(); - } - - public override void OnTextInput(TextInputEventArgs e) - { - if (base.IsEnabled && !IsReadOnly && (string.IsNullOrEmpty(e.Text) || e.Text.Length != 1 || e.Text[0] >= ' ') && !string.IsNullOrEmpty(e.Text)) - { - InsertText(e.Text); - e.Handled = true; - } - } - - private void InsertText(string text) - { - if (_selectionLength > 0) - { - string text2 = Text; - Text = text2.Remove(_selectionStart, _selectionLength); - _cursorPosition = _selectionStart; - _selectionStart = -1; - _selectionLength = 0; - } - if (MaxLength > 0 && Text.Length + text.Length > MaxLength) - { - text = text.Substring(0, Math.Max(0, MaxLength - Text.Length)); - } - if (!string.IsNullOrEmpty(text)) - { - Text = Text.Insert(_cursorPosition, text); - _cursorPosition += text.Length; - EnsureCursorVisible(); - } - } - - public override void OnScroll(ScrollEventArgs e) - { - //IL_001e: Unknown result type (might be due to invalid IL or missing references) - //IL_0023: Unknown result type (might be due to invalid IL or missing references) - float num = FontSize * LineHeight; - float num2 = (float)_lines.Count * num; - SKRect bounds = base.Bounds; - float num3 = ((SKRect)(ref bounds)).Height - Padding * 2f; - float max = Math.Max(0f, num2 - num3); - _scrollOffsetY = Math.Clamp(_scrollOffsetY - e.DeltaY * 3f, 0f, max); - Invalidate(); - } - - public override void OnFocusGained() - { - base.OnFocusGained(); - SkiaVisualStateManager.GoToState(this, "Focused"); - } - - public override void OnFocusLost() - { - base.OnFocusLost(); - SkiaVisualStateManager.GoToState(this, "Normal"); - } - - public void SelectAll() - { - _selectionStart = 0; - _cursorPosition = Text.Length; - _selectionLength = Text.Length; - Invalidate(); - } - - private void SelectWordAtCursor() - { - if (!string.IsNullOrEmpty(Text)) - { - int num = _cursorPosition; - int i = _cursorPosition; - while (num > 0 && IsWordChar(Text[num - 1])) - { - num--; - } - for (; i < Text.Length && IsWordChar(Text[i]); i++) - { - } - _selectionStart = num; - _cursorPosition = i; - _selectionLength = i - num; - } - } - - private static bool IsWordChar(char c) - { - if (!char.IsLetterOrDigit(c)) - { - return c == '_'; - } - return true; - } - - private void CopyToClipboard() - { - if (_selectionLength != 0) - { - int startIndex = Math.Min(_selectionStart, _selectionStart + _selectionLength); - int length = Math.Abs(_selectionLength); - SystemClipboard.SetText(Text.Substring(startIndex, length)); - } - } - - private void CutToClipboard() - { - CopyToClipboard(); - DeleteSelection(); - Invalidate(); - } - - private void PasteFromClipboard() - { - string text = SystemClipboard.GetText(); - if (!string.IsNullOrEmpty(text)) - { - if (_selectionLength != 0) - { - DeleteSelection(); - } - InsertText(text); - } - } - - private void DeleteSelection() - { - if (_selectionLength != 0) - { - int num = Math.Min(_selectionStart, _selectionStart + _selectionLength); - int count = Math.Abs(_selectionLength); - Text = Text.Remove(num, count); - _cursorPosition = num; - _selectionStart = -1; - _selectionLength = 0; - } - } - - protected override SKSize MeasureOverride(SKSize availableSize) - { - //IL_00d9: Unknown result type (might be due to invalid IL or missing references) - //IL_0087: Unknown result type (might be due to invalid IL or missing references) - if (AutoSize) - { - float num = FontSize * LineHeight; - float val = Math.Max(num + Padding * 2f, (float)_lines.Count * num + Padding * 2f); - return new SKSize((((SKSize)(ref availableSize)).Width < float.MaxValue) ? ((SKSize)(ref availableSize)).Width : 200f, Math.Min(val, (((SKSize)(ref availableSize)).Height < float.MaxValue) ? ((SKSize)(ref availableSize)).Height : 200f)); - } - return new SKSize((((SKSize)(ref availableSize)).Width < float.MaxValue) ? Math.Min(((SKSize)(ref availableSize)).Width, 200f) : 200f, (((SKSize)(ref availableSize)).Height < float.MaxValue) ? Math.Min(((SKSize)(ref availableSize)).Height, 150f) : 150f); - } + #region BindableProperties + + /// + /// Bindable property for Text. + /// + public static readonly BindableProperty TextProperty = + BindableProperty.Create( + nameof(Text), + typeof(string), + typeof(SkiaEditor), + "", + BindingMode.TwoWay, + propertyChanged: (b, o, n) => ((SkiaEditor)b).OnTextPropertyChanged((string)o, (string)n)); + + /// + /// Bindable property for Placeholder. + /// + public static readonly BindableProperty PlaceholderProperty = + BindableProperty.Create( + nameof(Placeholder), + typeof(string), + typeof(SkiaEditor), + "", + propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate()); + + /// + /// Bindable property for TextColor. + /// + public static readonly BindableProperty TextColorProperty = + BindableProperty.Create( + nameof(TextColor), + typeof(SKColor), + typeof(SkiaEditor), + SKColors.Black, + propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate()); + + /// + /// Bindable property for PlaceholderColor. + /// + public static readonly BindableProperty PlaceholderColorProperty = + BindableProperty.Create( + nameof(PlaceholderColor), + typeof(SKColor), + typeof(SkiaEditor), + new SKColor(0x80, 0x80, 0x80), + propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate()); + + /// + /// Bindable property for BorderColor. + /// + public static readonly BindableProperty BorderColorProperty = + BindableProperty.Create( + nameof(BorderColor), + typeof(SKColor), + typeof(SkiaEditor), + new SKColor(0xBD, 0xBD, 0xBD), + propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate()); + + /// + /// Bindable property for SelectionColor. + /// + public static readonly BindableProperty SelectionColorProperty = + BindableProperty.Create( + nameof(SelectionColor), + typeof(SKColor), + typeof(SkiaEditor), + new SKColor(0x21, 0x96, 0xF3, 0x60), + propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate()); + + /// + /// Bindable property for CursorColor. + /// + public static readonly BindableProperty CursorColorProperty = + BindableProperty.Create( + nameof(CursorColor), + typeof(SKColor), + typeof(SkiaEditor), + new SKColor(0x21, 0x96, 0xF3), + propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate()); + + /// + /// Bindable property for FontFamily. + /// + public static readonly BindableProperty FontFamilyProperty = + BindableProperty.Create( + nameof(FontFamily), + typeof(string), + typeof(SkiaEditor), + "Sans", + propertyChanged: (b, o, n) => ((SkiaEditor)b).InvalidateMeasure()); + + /// + /// Bindable property for FontSize. + /// + public static readonly BindableProperty FontSizeProperty = + BindableProperty.Create( + nameof(FontSize), + typeof(float), + typeof(SkiaEditor), + 14f, + propertyChanged: (b, o, n) => ((SkiaEditor)b).InvalidateMeasure()); + + /// + /// Bindable property for LineHeight. + /// + public static readonly BindableProperty LineHeightProperty = + BindableProperty.Create( + nameof(LineHeight), + typeof(float), + typeof(SkiaEditor), + 1.4f, + propertyChanged: (b, o, n) => ((SkiaEditor)b).InvalidateMeasure()); + + /// + /// Bindable property for CornerRadius. + /// + public static readonly BindableProperty CornerRadiusProperty = + BindableProperty.Create( + nameof(CornerRadius), + typeof(float), + typeof(SkiaEditor), + 4f, + propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate()); + + /// + /// Bindable property for Padding. + /// + public static readonly BindableProperty PaddingProperty = + BindableProperty.Create( + nameof(Padding), + typeof(float), + typeof(SkiaEditor), + 12f, + propertyChanged: (b, o, n) => ((SkiaEditor)b).InvalidateMeasure()); + + /// + /// Bindable property for IsReadOnly. + /// + public static readonly BindableProperty IsReadOnlyProperty = + BindableProperty.Create( + nameof(IsReadOnly), + typeof(bool), + typeof(SkiaEditor), + false, + propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate()); + + /// + /// Bindable property for MaxLength. + /// + public static readonly BindableProperty MaxLengthProperty = + BindableProperty.Create( + nameof(MaxLength), + typeof(int), + typeof(SkiaEditor), + -1); + + /// + /// Bindable property for AutoSize. + /// + public static readonly BindableProperty AutoSizeProperty = + BindableProperty.Create( + nameof(AutoSize), + typeof(bool), + typeof(SkiaEditor), + false, + propertyChanged: (b, o, n) => ((SkiaEditor)b).InvalidateMeasure()); + + #endregion + + #region Properties + + /// + /// Gets or sets the text content. + /// + public string Text + { + get => (string)GetValue(TextProperty); + set => SetValue(TextProperty, value); + } + + /// + /// Gets or sets the placeholder text. + /// + public string Placeholder + { + get => (string)GetValue(PlaceholderProperty); + set => SetValue(PlaceholderProperty, value); + } + + /// + /// Gets or sets the text color. + /// + public SKColor TextColor + { + get => (SKColor)GetValue(TextColorProperty); + set => SetValue(TextColorProperty, value); + } + + /// + /// Gets or sets the placeholder color. + /// + public SKColor PlaceholderColor + { + get => (SKColor)GetValue(PlaceholderColorProperty); + set => SetValue(PlaceholderColorProperty, value); + } + + /// + /// Gets or sets the border color. + /// + public SKColor BorderColor + { + get => (SKColor)GetValue(BorderColorProperty); + set => SetValue(BorderColorProperty, value); + } + + /// + /// Gets or sets the selection color. + /// + public SKColor SelectionColor + { + get => (SKColor)GetValue(SelectionColorProperty); + set => SetValue(SelectionColorProperty, value); + } + + /// + /// Gets or sets the cursor color. + /// + public SKColor CursorColor + { + get => (SKColor)GetValue(CursorColorProperty); + set => SetValue(CursorColorProperty, value); + } + + /// + /// Gets or sets the font family. + /// + public string FontFamily + { + get => (string)GetValue(FontFamilyProperty); + set => SetValue(FontFamilyProperty, value); + } + + /// + /// Gets or sets the font size. + /// + public float FontSize + { + get => (float)GetValue(FontSizeProperty); + set => SetValue(FontSizeProperty, value); + } + + /// + /// Gets or sets the line height multiplier. + /// + public float LineHeight + { + get => (float)GetValue(LineHeightProperty); + set => SetValue(LineHeightProperty, value); + } + + /// + /// Gets or sets the corner radius. + /// + public float CornerRadius + { + get => (float)GetValue(CornerRadiusProperty); + set => SetValue(CornerRadiusProperty, value); + } + + /// + /// Gets or sets the padding. + /// + public float Padding + { + get => (float)GetValue(PaddingProperty); + set => SetValue(PaddingProperty, value); + } + + /// + /// Gets or sets whether the editor is read-only. + /// + public bool IsReadOnly + { + get => (bool)GetValue(IsReadOnlyProperty); + set => SetValue(IsReadOnlyProperty, value); + } + + /// + /// Gets or sets the maximum length. -1 for unlimited. + /// + public int MaxLength + { + get => (int)GetValue(MaxLengthProperty); + set => SetValue(MaxLengthProperty, value); + } + + /// + /// Gets or sets whether the editor auto-sizes to content. + /// + public bool AutoSize + { + get => (bool)GetValue(AutoSizeProperty); + set => SetValue(AutoSizeProperty, value); + } + + /// + /// Gets or sets the cursor position. + /// + public int CursorPosition + { + get => _cursorPosition; + set + { + _cursorPosition = Math.Clamp(value, 0, Text.Length); + EnsureCursorVisible(); + Invalidate(); + } + } + + #endregion + + private int _cursorPosition; + private int _selectionStart = -1; + private int _selectionLength; + private float _scrollOffsetY; + private bool _cursorVisible = true; + private DateTime _lastCursorBlink = DateTime.Now; + private List _lines = new() { "" }; + private float _wrapWidth = 0; // Available width for word wrapping + private bool _isSelecting; // For mouse-based text selection + private DateTime _lastClickTime = DateTime.MinValue; + private float _lastClickX; + private float _lastClickY; + private const double DoubleClickThresholdMs = 400; + + /// + /// Event raised when text changes. + /// + public event EventHandler? TextChanged; + + /// + /// Event raised when editing is completed. + /// + public event EventHandler? Completed; + + public SkiaEditor() + { + IsFocusable = true; + } + + private void OnTextPropertyChanged(string oldText, string newText) + { + var text = newText ?? ""; + + if (MaxLength > 0 && text.Length > MaxLength) + { + text = text.Substring(0, MaxLength); + SetValue(TextProperty, text); + return; + } + + UpdateLines(); + _cursorPosition = Math.Min(_cursorPosition, text.Length); + _scrollOffsetY = 0; // Reset scroll when text changes externally + _selectionLength = 0; + TextChanged?.Invoke(this, EventArgs.Empty); + Invalidate(); + } + + private void UpdateLines() + { + _lines.Clear(); + var text = Text ?? ""; + if (string.IsNullOrEmpty(text)) + { + _lines.Add(""); + return; + } + + using var font = new SKFont(SKTypeface.Default, FontSize); + + // Split by actual newlines first + var paragraphs = text.Split('\n'); + + foreach (var paragraph in paragraphs) + { + if (string.IsNullOrEmpty(paragraph)) + { + _lines.Add(""); + continue; + } + + // Word wrap this paragraph if we have a known width + if (_wrapWidth > 0) + { + WrapParagraph(paragraph, font, _wrapWidth); + } + else + { + _lines.Add(paragraph); + } + } + + if (_lines.Count == 0) + { + _lines.Add(""); + } + } + + private void WrapParagraph(string paragraph, SKFont font, float maxWidth) + { + var words = paragraph.Split(' '); + var currentLine = ""; + + foreach (var word in words) + { + var testLine = string.IsNullOrEmpty(currentLine) ? word : currentLine + " " + word; + var lineWidth = MeasureText(testLine, font); + + if (lineWidth > maxWidth && !string.IsNullOrEmpty(currentLine)) + { + // Line too long, save current and start new + _lines.Add(currentLine); + currentLine = word; + } + else + { + currentLine = testLine; + } + } + + // Add remaining text + if (!string.IsNullOrEmpty(currentLine)) + { + _lines.Add(currentLine); + } + } + + private (int line, int column) GetLineColumn(int position) + { + var pos = 0; + for (int i = 0; i < _lines.Count; i++) + { + var lineLength = _lines[i].Length; + if (pos + lineLength >= position || i == _lines.Count - 1) + { + return (i, position - pos); + } + pos += lineLength + 1; + } + return (_lines.Count - 1, _lines[^1].Length); + } + + private int GetPosition(int line, int column) + { + var pos = 0; + for (int i = 0; i < line && i < _lines.Count; i++) + { + pos += _lines[i].Length + 1; + } + if (line < _lines.Count) + { + pos += Math.Min(column, _lines[line].Length); + } + return Math.Min(pos, Text.Length); + } + + protected override void OnDraw(SKCanvas canvas, SKRect bounds) + { + // Update wrap width if bounds changed and re-wrap text + var newWrapWidth = bounds.Width - Padding * 2; + if (Math.Abs(newWrapWidth - _wrapWidth) > 1) + { + _wrapWidth = newWrapWidth; + UpdateLines(); + } + + // Handle cursor blinking + if (IsFocused && (DateTime.Now - _lastCursorBlink).TotalMilliseconds > 500) + { + _cursorVisible = !_cursorVisible; + _lastCursorBlink = DateTime.Now; + } + + // Draw background + using var bgPaint = new SKPaint + { + Color = IsEnabled ? BackgroundColor : new SKColor(0xF5, 0xF5, 0xF5), + Style = SKPaintStyle.Fill, + IsAntialias = true + }; + canvas.DrawRoundRect(new SKRoundRect(bounds, CornerRadius), bgPaint); + + // Draw border + using var borderPaint = new SKPaint + { + Color = IsFocused ? CursorColor : 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); + var lineSpacing = FontSize * LineHeight; + + // Clip to content area + var contentRect = new SKRect( + bounds.Left + Padding, + bounds.Top + Padding, + bounds.Right - Padding, + bounds.Bottom - Padding); + + canvas.Save(); + canvas.ClipRect(contentRect); + // Don't translate - let the text draw at absolute positions + // canvas.Translate(0, -_scrollOffsetY); + + if (string.IsNullOrEmpty(Text) && !string.IsNullOrEmpty(Placeholder)) + { + using var placeholderPaint = new SKPaint(font) + { + Color = PlaceholderColor, + IsAntialias = true + }; + canvas.DrawText(Placeholder, contentRect.Left, contentRect.Top + FontSize, placeholderPaint); + } + else + { + using var textPaint = new SKPaint(font) + { + Color = IsEnabled ? TextColor : TextColor.WithAlpha(128), + IsAntialias = true + }; + using var selectionPaint = new SKPaint + { + Color = SelectionColor, + Style = SKPaintStyle.Fill + }; + + var y = contentRect.Top + FontSize; + var charIndex = 0; + + for (int lineIndex = 0; lineIndex < _lines.Count; lineIndex++) + { + var line = _lines[lineIndex]; + var x = contentRect.Left; + + // Draw selection for this line if applicable + if (_selectionStart >= 0 && _selectionLength != 0) + { + // Handle both positive and negative selection lengths + var selStart = _selectionLength > 0 ? _selectionStart : _selectionStart + _selectionLength; + var selEnd = _selectionLength > 0 ? _selectionStart + _selectionLength : _selectionStart; + var lineStart = charIndex; + var lineEnd = charIndex + line.Length; + + if (selEnd > lineStart && selStart < lineEnd) + { + var selStartInLine = Math.Max(0, selStart - lineStart); + var selEndInLine = Math.Min(line.Length, selEnd - lineStart); + + var startX = x + MeasureText(line.Substring(0, selStartInLine), font); + var endX = x + MeasureText(line.Substring(0, selEndInLine), font); + + canvas.DrawRect(new SKRect(startX, y - FontSize, endX, y + lineSpacing - FontSize), selectionPaint); + } + } + + canvas.DrawText(line, x, y, textPaint); + + // Draw cursor if on this line + if (IsFocused && _cursorVisible) + { + var (cursorLine, cursorCol) = GetLineColumn(_cursorPosition); + if (cursorLine == lineIndex) + { + var cursorX = x + MeasureText(line.Substring(0, Math.Min(cursorCol, line.Length)), font); + using var cursorPaint = new SKPaint + { + Color = CursorColor, + Style = SKPaintStyle.Stroke, + StrokeWidth = 2, + IsAntialias = true + }; + canvas.DrawLine(cursorX, y - FontSize + 2, cursorX, y + 2, cursorPaint); + } + } + + y += lineSpacing; + charIndex += line.Length + 1; + } + } + + canvas.Restore(); + + // Draw scrollbar if needed + var totalHeight = _lines.Count * FontSize * LineHeight; + if (totalHeight > contentRect.Height) + { + DrawScrollbar(canvas, bounds, contentRect.Height, totalHeight); + } + } + + private float MeasureText(string text, SKFont font) + { + if (string.IsNullOrEmpty(text)) return 0; + using var paint = new SKPaint(font); + return paint.MeasureText(text); + } + + private void DrawScrollbar(SKCanvas canvas, SKRect bounds, float viewHeight, float contentHeight) + { + var scrollbarWidth = 6f; + var scrollbarMargin = 2f; + var scrollbarHeight = Math.Max(20, viewHeight * (viewHeight / contentHeight)); + var scrollbarY = bounds.Top + Padding + (_scrollOffsetY / contentHeight) * (viewHeight - scrollbarHeight); + + using var paint = new SKPaint + { + Color = new SKColor(0, 0, 0, 60), + Style = SKPaintStyle.Fill, + IsAntialias = true + }; + + canvas.DrawRoundRect(new SKRoundRect( + new SKRect( + bounds.Right - scrollbarWidth - scrollbarMargin, + scrollbarY, + bounds.Right - scrollbarMargin, + scrollbarY + scrollbarHeight), + scrollbarWidth / 2), paint); + } + + private void EnsureCursorVisible() + { + var (line, col) = GetLineColumn(_cursorPosition); + var lineSpacing = FontSize * LineHeight; + var cursorY = line * lineSpacing; + var viewHeight = Bounds.Height - Padding * 2; + + if (cursorY < _scrollOffsetY) + { + _scrollOffsetY = cursorY; + } + else if (cursorY + lineSpacing > _scrollOffsetY + viewHeight) + { + _scrollOffsetY = cursorY + lineSpacing - viewHeight; + } + } + + public override void OnPointerPressed(PointerEventArgs e) + { + if (!IsEnabled) return; + + IsFocused = true; + + // Use screen coordinates for proper hit detection + var screenBounds = ScreenBounds; + var contentX = e.X - screenBounds.Left - Padding; + var contentY = e.Y - screenBounds.Top - Padding + _scrollOffsetY; + + var lineSpacing = FontSize * LineHeight; + var clickedLine = Math.Clamp((int)(contentY / lineSpacing), 0, _lines.Count - 1); + + using var font = new SKFont(SKTypeface.Default, FontSize); + var line = _lines[clickedLine]; + var clickedCol = 0; + + for (int i = 0; i <= line.Length; i++) + { + var charX = MeasureText(line.Substring(0, i), font); + if (charX > contentX) + { + clickedCol = i > 0 ? i - 1 : 0; + break; + } + clickedCol = i; + } + + _cursorPosition = GetPosition(clickedLine, clickedCol); + + // Check for double-click (select word) + var now = DateTime.UtcNow; + var timeSinceLastClick = (now - _lastClickTime).TotalMilliseconds; + var distanceFromLastClick = Math.Sqrt(Math.Pow(e.X - _lastClickX, 2) + Math.Pow(e.Y - _lastClickY, 2)); + + if (timeSinceLastClick < DoubleClickThresholdMs && distanceFromLastClick < 10) + { + // Double-click: select the word at cursor + SelectWordAtCursor(); + _lastClickTime = DateTime.MinValue; // Reset to prevent triple-click issues + _isSelecting = false; + } + else + { + // Single click: start selection + _selectionStart = _cursorPosition; + _selectionLength = 0; + _isSelecting = true; + _lastClickTime = now; + _lastClickX = e.X; + _lastClickY = e.Y; + } + + _cursorVisible = true; + _lastCursorBlink = DateTime.Now; + + Invalidate(); + } + + public override void OnPointerMoved(PointerEventArgs e) + { + if (!IsEnabled || !_isSelecting) return; + + // Calculate position from mouse coordinates + var screenBounds = ScreenBounds; + var contentX = e.X - screenBounds.Left - Padding; + var contentY = e.Y - screenBounds.Top - Padding + _scrollOffsetY; + + var lineSpacing = FontSize * LineHeight; + var clickedLine = Math.Clamp((int)(contentY / lineSpacing), 0, _lines.Count - 1); + + using var font = new SKFont(SKTypeface.Default, FontSize); + var line = _lines[clickedLine]; + var clickedCol = 0; + + for (int i = 0; i <= line.Length; i++) + { + var charX = MeasureText(line.Substring(0, i), font); + if (charX > contentX) + { + clickedCol = i > 0 ? i - 1 : 0; + break; + } + clickedCol = i; + } + + var newPosition = GetPosition(clickedLine, clickedCol); + if (newPosition != _cursorPosition) + { + _cursorPosition = newPosition; + _selectionLength = _cursorPosition - _selectionStart; + _cursorVisible = true; + _lastCursorBlink = DateTime.Now; + Invalidate(); + } + } + + public override void OnPointerReleased(PointerEventArgs e) + { + _isSelecting = false; + } + + public override void OnKeyDown(KeyEventArgs e) + { + if (!IsEnabled) return; + + var (line, col) = GetLineColumn(_cursorPosition); + _cursorVisible = true; + _lastCursorBlink = DateTime.Now; + + switch (e.Key) + { + case Key.Left: + if (_cursorPosition > 0) + { + _cursorPosition--; + EnsureCursorVisible(); + } + e.Handled = true; + break; + + case Key.Right: + if (_cursorPosition < Text.Length) + { + _cursorPosition++; + EnsureCursorVisible(); + } + e.Handled = true; + break; + + case Key.Up: + if (line > 0) + { + _cursorPosition = GetPosition(line - 1, col); + EnsureCursorVisible(); + } + e.Handled = true; + break; + + case Key.Down: + if (line < _lines.Count - 1) + { + _cursorPosition = GetPosition(line + 1, col); + EnsureCursorVisible(); + } + e.Handled = true; + break; + + case Key.Home: + _cursorPosition = GetPosition(line, 0); + EnsureCursorVisible(); + e.Handled = true; + break; + + case Key.End: + _cursorPosition = GetPosition(line, _lines[line].Length); + EnsureCursorVisible(); + e.Handled = true; + break; + + case Key.Enter: + if (!IsReadOnly) + { + InsertText("\n"); + } + e.Handled = true; + break; + + case Key.Backspace: + if (!IsReadOnly) + { + if (_selectionLength != 0) + { + DeleteSelection(); + } + else if (_cursorPosition > 0) + { + Text = Text.Remove(_cursorPosition - 1, 1); + _cursorPosition--; + } + EnsureCursorVisible(); + } + e.Handled = true; + break; + + case Key.Delete: + if (!IsReadOnly) + { + if (_selectionLength != 0) + { + DeleteSelection(); + } + else if (_cursorPosition < Text.Length) + { + Text = Text.Remove(_cursorPosition, 1); + } + } + e.Handled = true; + break; + + case Key.Tab: + if (!IsReadOnly) + { + InsertText(" "); + } + e.Handled = true; + break; + + case Key.A: + if (e.Modifiers.HasFlag(KeyModifiers.Control)) + { + SelectAll(); + e.Handled = true; + } + break; + + case Key.C: + if (e.Modifiers.HasFlag(KeyModifiers.Control)) + { + CopyToClipboard(); + e.Handled = true; + } + break; + + case Key.V: + if (e.Modifiers.HasFlag(KeyModifiers.Control) && !IsReadOnly) + { + PasteFromClipboard(); + e.Handled = true; + } + break; + + case Key.X: + if (e.Modifiers.HasFlag(KeyModifiers.Control) && !IsReadOnly) + { + CutToClipboard(); + e.Handled = true; + } + break; + } + + Invalidate(); + } + + public override void OnTextInput(TextInputEventArgs e) + { + if (!IsEnabled || IsReadOnly) return; + + // Ignore control characters (Ctrl+key combinations send ASCII control codes) + if (!string.IsNullOrEmpty(e.Text) && e.Text.Length == 1 && e.Text[0] < 32) + return; + + if (!string.IsNullOrEmpty(e.Text)) + { + InsertText(e.Text); + e.Handled = true; + } + } + + private void InsertText(string text) + { + if (_selectionLength > 0) + { + var currentText = Text; + Text = currentText.Remove(_selectionStart, _selectionLength); + _cursorPosition = _selectionStart; + _selectionStart = -1; + _selectionLength = 0; + } + + if (MaxLength > 0 && Text.Length + text.Length > MaxLength) + { + text = text.Substring(0, Math.Max(0, MaxLength - Text.Length)); + } + + if (!string.IsNullOrEmpty(text)) + { + Text = Text.Insert(_cursorPosition, text); + _cursorPosition += text.Length; + EnsureCursorVisible(); + } + } + + public override void OnScroll(ScrollEventArgs e) + { + var lineSpacing = FontSize * LineHeight; + var totalHeight = _lines.Count * lineSpacing; + var viewHeight = Bounds.Height - Padding * 2; + var maxScroll = Math.Max(0, totalHeight - viewHeight); + + _scrollOffsetY = Math.Clamp(_scrollOffsetY - e.DeltaY * 3, 0, maxScroll); + Invalidate(); + } + + public override void OnFocusGained() + { + base.OnFocusGained(); + SkiaVisualStateManager.GoToState(this, SkiaVisualStateManager.CommonStates.Focused); + } + + public override void OnFocusLost() + { + base.OnFocusLost(); + SkiaVisualStateManager.GoToState(this, SkiaVisualStateManager.CommonStates.Normal); + } + + #region Selection and Clipboard + + public void SelectAll() + { + _selectionStart = 0; + _cursorPosition = Text.Length; + _selectionLength = Text.Length; + Invalidate(); + } + + private void SelectWordAtCursor() + { + if (string.IsNullOrEmpty(Text)) return; + + // Find word boundaries + int start = _cursorPosition; + int end = _cursorPosition; + + // Move start backwards to beginning of word + while (start > 0 && IsWordChar(Text[start - 1])) + start--; + + // Move end forwards to end of word + while (end < Text.Length && IsWordChar(Text[end])) + end++; + + _selectionStart = start; + _cursorPosition = end; + _selectionLength = end - start; + } + + private static bool IsWordChar(char c) + { + return char.IsLetterOrDigit(c) || c == '_'; + } + + private void CopyToClipboard() + { + if (_selectionLength == 0) return; + + var start = Math.Min(_selectionStart, _selectionStart + _selectionLength); + var length = Math.Abs(_selectionLength); + var selectedText = Text.Substring(start, length); + + // Use system clipboard via xclip/xsel + SystemClipboard.SetText(selectedText); + } + + private void CutToClipboard() + { + CopyToClipboard(); + DeleteSelection(); + Invalidate(); + } + + private void PasteFromClipboard() + { + // Get from system clipboard + var text = SystemClipboard.GetText(); + if (string.IsNullOrEmpty(text)) return; + + if (_selectionLength != 0) + { + DeleteSelection(); + } + + InsertText(text); + } + + private void DeleteSelection() + { + if (_selectionLength == 0) return; + + var start = Math.Min(_selectionStart, _selectionStart + _selectionLength); + var length = Math.Abs(_selectionLength); + + Text = Text.Remove(start, length); + _cursorPosition = start; + _selectionStart = -1; + _selectionLength = 0; + } + + #endregion + + protected override SKSize MeasureOverride(SKSize availableSize) + { + if (AutoSize) + { + var lineSpacing = FontSize * LineHeight; + var height = Math.Max(lineSpacing + Padding * 2, _lines.Count * lineSpacing + Padding * 2); + return new SKSize( + availableSize.Width < float.MaxValue ? availableSize.Width : 200, + (float)Math.Min(height, availableSize.Height < float.MaxValue ? availableSize.Height : 200)); + } + + return new SKSize( + availableSize.Width < float.MaxValue ? Math.Min(availableSize.Width, 200) : 200, + availableSize.Height < float.MaxValue ? Math.Min(availableSize.Height, 150) : 150); + } } diff --git a/Views/SkiaEntry.cs b/Views/SkiaEntry.cs index 7c4c225..22fb8f0 100644 --- a/Views/SkiaEntry.cs +++ b/Views/SkiaEntry.cs @@ -1,1313 +1,1264 @@ -using System; -using System.Collections.Generic; -using Microsoft.Maui.Controls; -using Microsoft.Maui.Platform.Linux.Rendering; -using Microsoft.Maui.Platform.Linux.Services; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using SkiaSharp; +using Microsoft.Maui.Platform.Linux.Rendering; namespace Microsoft.Maui.Platform; +/// +/// Skia-rendered text entry control with full XAML styling and data binding support. +/// public class SkiaEntry : SkiaView { - public static readonly BindableProperty TextProperty = BindableProperty.Create("Text", typeof(string), typeof(SkiaEntry), (object)"", (BindingMode)1, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaEntry)(object)b).OnTextPropertyChanged((string)o, (string)n); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty PlaceholderProperty = BindableProperty.Create("Placeholder", typeof(string), typeof(SkiaEntry), (object)"", (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaEntry)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty PlaceholderColorProperty = BindableProperty.Create("PlaceholderColor", typeof(SKColor), typeof(SkiaEntry), (object)new SKColor((byte)158, (byte)158, (byte)158), (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaEntry)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty TextColorProperty = BindableProperty.Create("TextColor", typeof(SKColor), typeof(SkiaEntry), (object)SKColors.Black, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaEntry)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty EntryBackgroundColorProperty = BindableProperty.Create("EntryBackgroundColor", typeof(SKColor), typeof(SkiaEntry), (object)SKColors.White, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaEntry)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty BorderColorProperty = BindableProperty.Create("BorderColor", typeof(SKColor), typeof(SkiaEntry), (object)new SKColor((byte)189, (byte)189, (byte)189), (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaEntry)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty FocusedBorderColorProperty = BindableProperty.Create("FocusedBorderColor", typeof(SKColor), typeof(SkiaEntry), (object)new SKColor((byte)33, (byte)150, (byte)243), (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaEntry)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty SelectionColorProperty = BindableProperty.Create("SelectionColor", typeof(SKColor), typeof(SkiaEntry), (object)new SKColor((byte)33, (byte)150, (byte)243, (byte)128), (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaEntry)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty CursorColorProperty = BindableProperty.Create("CursorColor", typeof(SKColor), typeof(SkiaEntry), (object)new SKColor((byte)33, (byte)150, (byte)243), (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaEntry)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty FontFamilyProperty = BindableProperty.Create("FontFamily", typeof(string), typeof(SkiaEntry), (object)"Sans", (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaEntry)(object)b).InvalidateMeasure(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty FontSizeProperty = BindableProperty.Create("FontSize", typeof(float), typeof(SkiaEntry), (object)14f, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaEntry)(object)b).InvalidateMeasure(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty IsBoldProperty = BindableProperty.Create("IsBold", typeof(bool), typeof(SkiaEntry), (object)false, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaEntry)(object)b).InvalidateMeasure(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty IsItalicProperty = BindableProperty.Create("IsItalic", typeof(bool), typeof(SkiaEntry), (object)false, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaEntry)(object)b).InvalidateMeasure(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty CornerRadiusProperty = BindableProperty.Create("CornerRadius", typeof(float), typeof(SkiaEntry), (object)4f, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaEntry)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty BorderWidthProperty = BindableProperty.Create("BorderWidth", typeof(float), typeof(SkiaEntry), (object)1f, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaEntry)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty PaddingProperty = BindableProperty.Create("Padding", typeof(SKRect), typeof(SkiaEntry), (object)new SKRect(12f, 8f, 12f, 8f), (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaEntry)(object)b).InvalidateMeasure(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty IsPasswordProperty = BindableProperty.Create("IsPassword", typeof(bool), typeof(SkiaEntry), (object)false, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaEntry)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty PasswordCharProperty = BindableProperty.Create("PasswordChar", typeof(char), typeof(SkiaEntry), (object)'*', (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaEntry)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty MaxLengthProperty = BindableProperty.Create("MaxLength", typeof(int), typeof(SkiaEntry), (object)0, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)null, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty IsReadOnlyProperty = BindableProperty.Create("IsReadOnly", typeof(bool), typeof(SkiaEntry), (object)false, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaEntry)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty HorizontalTextAlignmentProperty = BindableProperty.Create("HorizontalTextAlignment", typeof(TextAlignment), typeof(SkiaEntry), (object)TextAlignment.Start, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaEntry)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty VerticalTextAlignmentProperty = BindableProperty.Create("VerticalTextAlignment", typeof(TextAlignment), typeof(SkiaEntry), (object)TextAlignment.Center, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaEntry)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty ShowClearButtonProperty = BindableProperty.Create("ShowClearButton", typeof(bool), typeof(SkiaEntry), (object)false, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaEntry)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty CharacterSpacingProperty = BindableProperty.Create("CharacterSpacing", typeof(float), typeof(SkiaEntry), (object)0f, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaEntry)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - private int _cursorPosition; - - private int _selectionStart; - - private int _selectionLength; - - private float _scrollOffset; - - private DateTime _cursorBlinkTime = DateTime.UtcNow; - - private bool _cursorVisible = true; - - private bool _isSelecting; - - private DateTime _lastClickTime = DateTime.MinValue; - - private float _lastClickX; - - private const double DoubleClickThresholdMs = 400.0; - - public string Text - { - get - { - return (string)((BindableObject)this).GetValue(TextProperty); - } - set - { - ((BindableObject)this).SetValue(TextProperty, (object)value); - } - } - - public string Placeholder - { - get - { - return (string)((BindableObject)this).GetValue(PlaceholderProperty); - } - set - { - ((BindableObject)this).SetValue(PlaceholderProperty, (object)value); - } - } - - public SKColor PlaceholderColor - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKColor)((BindableObject)this).GetValue(PlaceholderColorProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(PlaceholderColorProperty, (object)value); - } - } - - public SKColor TextColor - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKColor)((BindableObject)this).GetValue(TextColorProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(TextColorProperty, (object)value); - } - } - - public SKColor EntryBackgroundColor - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKColor)((BindableObject)this).GetValue(EntryBackgroundColorProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(EntryBackgroundColorProperty, (object)value); - } - } - - public SKColor BorderColor - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKColor)((BindableObject)this).GetValue(BorderColorProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(BorderColorProperty, (object)value); - } - } - - public SKColor FocusedBorderColor - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKColor)((BindableObject)this).GetValue(FocusedBorderColorProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(FocusedBorderColorProperty, (object)value); - } - } - - public SKColor SelectionColor - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKColor)((BindableObject)this).GetValue(SelectionColorProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(SelectionColorProperty, (object)value); - } - } - - public SKColor CursorColor - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKColor)((BindableObject)this).GetValue(CursorColorProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(CursorColorProperty, (object)value); - } - } - - public string FontFamily - { - get - { - return (string)((BindableObject)this).GetValue(FontFamilyProperty); - } - set - { - ((BindableObject)this).SetValue(FontFamilyProperty, (object)value); - } - } - - public float FontSize - { - get - { - return (float)((BindableObject)this).GetValue(FontSizeProperty); - } - set - { - ((BindableObject)this).SetValue(FontSizeProperty, (object)value); - } - } - - public bool IsBold - { - get - { - return (bool)((BindableObject)this).GetValue(IsBoldProperty); - } - set - { - ((BindableObject)this).SetValue(IsBoldProperty, (object)value); - } - } - - public bool IsItalic - { - get - { - return (bool)((BindableObject)this).GetValue(IsItalicProperty); - } - set - { - ((BindableObject)this).SetValue(IsItalicProperty, (object)value); - } - } - - public float CornerRadius - { - get - { - return (float)((BindableObject)this).GetValue(CornerRadiusProperty); - } - set - { - ((BindableObject)this).SetValue(CornerRadiusProperty, (object)value); - } - } - - public float BorderWidth - { - get - { - return (float)((BindableObject)this).GetValue(BorderWidthProperty); - } - set - { - ((BindableObject)this).SetValue(BorderWidthProperty, (object)value); - } - } - - public SKRect Padding - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKRect)((BindableObject)this).GetValue(PaddingProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(PaddingProperty, (object)value); - } - } - - public bool IsPassword - { - get - { - return (bool)((BindableObject)this).GetValue(IsPasswordProperty); - } - set - { - ((BindableObject)this).SetValue(IsPasswordProperty, (object)value); - } - } - - public char PasswordChar - { - get - { - return (char)((BindableObject)this).GetValue(PasswordCharProperty); - } - set - { - ((BindableObject)this).SetValue(PasswordCharProperty, (object)value); - } - } - - public int MaxLength - { - get - { - return (int)((BindableObject)this).GetValue(MaxLengthProperty); - } - set - { - ((BindableObject)this).SetValue(MaxLengthProperty, (object)value); - } - } - - public bool IsReadOnly - { - get - { - return (bool)((BindableObject)this).GetValue(IsReadOnlyProperty); - } - set - { - ((BindableObject)this).SetValue(IsReadOnlyProperty, (object)value); - } - } - - public TextAlignment HorizontalTextAlignment - { - get - { - return (TextAlignment)((BindableObject)this).GetValue(HorizontalTextAlignmentProperty); - } - set - { - ((BindableObject)this).SetValue(HorizontalTextAlignmentProperty, (object)value); - } - } - - public TextAlignment VerticalTextAlignment - { - get - { - return (TextAlignment)((BindableObject)this).GetValue(VerticalTextAlignmentProperty); - } - set - { - ((BindableObject)this).SetValue(VerticalTextAlignmentProperty, (object)value); - } - } - - public bool ShowClearButton - { - get - { - return (bool)((BindableObject)this).GetValue(ShowClearButtonProperty); - } - set - { - ((BindableObject)this).SetValue(ShowClearButtonProperty, (object)value); - } - } - - public float CharacterSpacing - { - get - { - return (float)((BindableObject)this).GetValue(CharacterSpacingProperty); - } - set - { - ((BindableObject)this).SetValue(CharacterSpacingProperty, (object)value); - } - } - - public int CursorPosition - { - get - { - return _cursorPosition; - } - set - { - _cursorPosition = Math.Clamp(value, 0, Text.Length); - ResetCursorBlink(); - Invalidate(); - } - } - - public int SelectionLength - { - get - { - return _selectionLength; - } - set - { - _selectionLength = value; - Invalidate(); - } - } - - public event EventHandler? TextChanged; - - public event EventHandler? Completed; - - public SkiaEntry() - { - base.IsFocusable = true; - } - - private void OnTextPropertyChanged(string oldText, string newText) - { - _cursorPosition = Math.Min(_cursorPosition, (newText ?? "").Length); - _scrollOffset = 0f; - _selectionLength = 0; - this.TextChanged?.Invoke(this, new TextChangedEventArgs(oldText, newText ?? "")); - Invalidate(); - } - - protected override void OnDraw(SKCanvas canvas, SKRect bounds) - { - //IL_0000: Unknown result type (might be due to invalid IL or missing references) - //IL_0005: Unknown result type (might be due to invalid IL or missing references) - //IL_0007: Unknown result type (might be due to invalid IL or missing references) - //IL_0011: Unknown result type (might be due to invalid IL or missing references) - //IL_0018: Unknown result type (might be due to invalid IL or missing references) - //IL_0020: Expected O, but got Unknown - //IL_0020: Unknown result type (might be due to invalid IL or missing references) - //IL_0027: Unknown result type (might be due to invalid IL or missing references) - //IL_002d: Expected O, but got Unknown - //IL_0046: Unknown result type (might be due to invalid IL or missing references) - //IL_003e: Unknown result type (might be due to invalid IL or missing references) - //IL_004b: Unknown result type (might be due to invalid IL or missing references) - //IL_0069: Unknown result type (might be due to invalid IL or missing references) - //IL_006e: Unknown result type (might be due to invalid IL or missing references) - //IL_006f: Unknown result type (might be due to invalid IL or missing references) - //IL_0075: Unknown result type (might be due to invalid IL or missing references) - //IL_007c: Unknown result type (might be due to invalid IL or missing references) - //IL_0083: Unknown result type (might be due to invalid IL or missing references) - //IL_008c: Expected O, but got Unknown - //IL_009d: Unknown result type (might be due to invalid IL or missing references) - //IL_00a2: Unknown result type (might be due to invalid IL or missing references) - //IL_00b4: Unknown result type (might be due to invalid IL or missing references) - //IL_00b9: Unknown result type (might be due to invalid IL or missing references) - //IL_00cb: Unknown result type (might be due to invalid IL or missing references) - //IL_00d0: Unknown result type (might be due to invalid IL or missing references) - //IL_00e2: Unknown result type (might be due to invalid IL or missing references) - //IL_00e7: Unknown result type (might be due to invalid IL or missing references) - //IL_00f1: Unknown result type (might be due to invalid IL or missing references) - //IL_00f6: Unknown result type (might be due to invalid IL or missing references) - //IL_013e: Unknown result type (might be due to invalid IL or missing references) - //IL_0186: Unknown result type (might be due to invalid IL or missing references) - //IL_018d: Expected O, but got Unknown - //IL_018f: Unknown result type (might be due to invalid IL or missing references) - //IL_0194: Unknown result type (might be due to invalid IL or missing references) - //IL_019d: Expected O, but got Unknown - //IL_01b7: Unknown result type (might be due to invalid IL or missing references) - //IL_0310: Unknown result type (might be due to invalid IL or missing references) - //IL_031c: Unknown result type (might be due to invalid IL or missing references) - //IL_024f: Unknown result type (might be due to invalid IL or missing references) - //IL_0381: Unknown result type (might be due to invalid IL or missing references) - //IL_0246: Unknown result type (might be due to invalid IL or missing references) - //IL_03ad: Unknown result type (might be due to invalid IL or missing references) - //IL_02f4: Unknown result type (might be due to invalid IL or missing references) - SKPaint val = new SKPaint - { - Color = EntryBackgroundColor, - IsAntialias = true, - Style = (SKPaintStyle)0 - }; - try - { - SKRoundRect val2 = new SKRoundRect(bounds, CornerRadius); - canvas.DrawRoundRect(val2, val); - SKColor color = (base.IsFocused ? FocusedBorderColor : BorderColor); - float strokeWidth = (base.IsFocused ? (BorderWidth + 1f) : BorderWidth); - SKPaint val3 = new SKPaint - { - Color = color, - IsAntialias = true, - Style = (SKPaintStyle)1, - StrokeWidth = strokeWidth - }; - try - { - canvas.DrawRoundRect(val2, val3); - float left = ((SKRect)(ref bounds)).Left; - SKRect padding = Padding; - float num = left + ((SKRect)(ref padding)).Left; - float top = ((SKRect)(ref bounds)).Top; - padding = Padding; - float num2 = top + ((SKRect)(ref padding)).Top; - float right = ((SKRect)(ref bounds)).Right; - padding = Padding; - float num3 = right - ((SKRect)(ref padding)).Right; - float bottom = ((SKRect)(ref bounds)).Bottom; - padding = Padding; - SKRect val4 = new SKRect(num, num2, num3, bottom - ((SKRect)(ref padding)).Bottom); - float num4 = 20f; - float num5 = 8f; - if (ShowClearButton && !string.IsNullOrEmpty(Text) && base.IsFocused) - { - ((SKRect)(ref val4)).Right = ((SKRect)(ref val4)).Right - (num4 + num5); - } - canvas.Save(); - canvas.ClipRect(val4, (SKClipOperation)1, false); - SKFontStyle fontStyle = GetFontStyle(); - SKFont val5 = new SKFont(SkiaRenderingEngine.Current?.ResourceCache.GetTypeface(FontFamily, fontStyle) ?? SKTypeface.Default, FontSize, 1f, 0f); - try - { - SKPaint val6 = new SKPaint(val5) - { - IsAntialias = true - }; - try - { - string displayText = GetDisplayText(); - if (!string.IsNullOrEmpty(displayText)) - { - val6.Color = TextColor; - string text = displayText.Substring(0, Math.Min(_cursorPosition, displayText.Length)); - float num6 = val6.MeasureText(text); - if (num6 - _scrollOffset > ((SKRect)(ref val4)).Width - 10f) - { - _scrollOffset = num6 - ((SKRect)(ref val4)).Width + 10f; - } - else if (num6 - _scrollOffset < 0f) - { - _scrollOffset = num6; - } - if (base.IsFocused && _selectionLength != 0) - { - DrawSelection(canvas, val6, displayText, val4); - } - SKRect val7 = default(SKRect); - val6.MeasureText(displayText, ref val7); - float num7 = ((SKRect)(ref val4)).Left - _scrollOffset; - canvas.DrawText(displayText, num7, VerticalTextAlignment switch - { - TextAlignment.Start => ((SKRect)(ref val4)).Top - ((SKRect)(ref val7)).Top, - TextAlignment.End => ((SKRect)(ref val4)).Bottom - ((SKRect)(ref val7)).Bottom, - _ => ((SKRect)(ref val4)).MidY - ((SKRect)(ref val7)).MidY, - }, val6); - if (base.IsFocused && !IsReadOnly && _cursorVisible) - { - DrawCursor(canvas, val6, displayText, val4); - } - } - else if (!string.IsNullOrEmpty(Placeholder)) - { - val6.Color = PlaceholderColor; - SKRect val8 = default(SKRect); - val6.MeasureText(Placeholder, ref val8); - float left2 = ((SKRect)(ref val4)).Left; - float num8 = ((SKRect)(ref val4)).MidY - ((SKRect)(ref val8)).MidY; - canvas.DrawText(Placeholder, left2, num8, val6); - } - else if (base.IsFocused && !IsReadOnly && _cursorVisible) - { - DrawCursor(canvas, val6, "", val4); - } - canvas.Restore(); - if (ShowClearButton && !string.IsNullOrEmpty(Text) && base.IsFocused) - { - DrawClearButton(canvas, bounds, num4, num5); - } - } - finally - { - ((IDisposable)val6)?.Dispose(); - } - } - finally - { - ((IDisposable)val5)?.Dispose(); - } - } - finally - { - ((IDisposable)val3)?.Dispose(); - } - } - finally - { - ((IDisposable)val)?.Dispose(); - } - } - - private SKFontStyle GetFontStyle() - { - if (IsBold && IsItalic) - { - return SKFontStyle.BoldItalic; - } - if (IsBold) - { - return SKFontStyle.Bold; - } - if (IsItalic) - { - return SKFontStyle.Italic; - } - return SKFontStyle.Normal; - } - - private void DrawClearButton(SKCanvas canvas, SKRect bounds, float size, float margin) - { - //IL_001b: Unknown result type (might be due to invalid IL or missing references) - //IL_0020: Unknown result type (might be due to invalid IL or missing references) - //IL_0030: Unknown result type (might be due to invalid IL or missing references) - //IL_003a: Unknown result type (might be due to invalid IL or missing references) - //IL_0041: Unknown result type (might be due to invalid IL or missing references) - //IL_0049: Expected O, but got Unknown - //IL_005f: Unknown result type (might be due to invalid IL or missing references) - //IL_0064: Unknown result type (might be due to invalid IL or missing references) - //IL_0065: Unknown result type (might be due to invalid IL or missing references) - //IL_006f: Unknown result type (might be due to invalid IL or missing references) - //IL_0076: Unknown result type (might be due to invalid IL or missing references) - //IL_007d: Unknown result type (might be due to invalid IL or missing references) - //IL_0088: Unknown result type (might be due to invalid IL or missing references) - //IL_0090: Expected O, but got Unknown - float num = ((SKRect)(ref bounds)).Right - margin - size / 2f; - float midY = ((SKRect)(ref bounds)).MidY; - SKPaint val = new SKPaint - { - Color = new SKColor((byte)189, (byte)189, (byte)189), - IsAntialias = true, - Style = (SKPaintStyle)0 - }; - try - { - canvas.DrawCircle(num, midY, size / 2f - 2f, val); - SKPaint val2 = new SKPaint - { - Color = SKColors.White, - IsAntialias = true, - Style = (SKPaintStyle)1, - StrokeWidth = 2f, - StrokeCap = (SKStrokeCap)1 - }; - try - { - float num2 = size / 4f - 1f; - canvas.DrawLine(num - num2, midY - num2, num + num2, midY + num2, val2); - canvas.DrawLine(num - num2, midY + num2, num + num2, midY - num2, val2); - } - finally - { - ((IDisposable)val2)?.Dispose(); - } - } - finally - { - ((IDisposable)val)?.Dispose(); - } - } - - private string GetDisplayText() - { - if (IsPassword && !string.IsNullOrEmpty(Text)) - { - return new string(PasswordChar, Text.Length); - } - return Text; - } - - private void DrawSelection(SKCanvas canvas, SKPaint paint, string displayText, SKRect bounds) - { - //IL_0074: Unknown result type (might be due to invalid IL or missing references) - //IL_0079: Unknown result type (might be due to invalid IL or missing references) - //IL_007b: Unknown result type (might be due to invalid IL or missing references) - //IL_0085: Unknown result type (might be due to invalid IL or missing references) - //IL_008e: Expected O, but got Unknown - int length = Math.Min(_selectionStart, _selectionStart + _selectionLength); - int length2 = Math.Max(_selectionStart, _selectionStart + _selectionLength); - string text = displayText.Substring(0, length); - string text2 = displayText.Substring(0, length2); - float num = ((SKRect)(ref bounds)).Left - _scrollOffset + paint.MeasureText(text); - float num2 = ((SKRect)(ref bounds)).Left - _scrollOffset + paint.MeasureText(text2); - SKPaint val = new SKPaint - { - Color = SelectionColor, - Style = (SKPaintStyle)0 - }; - try - { - canvas.DrawRect(num, ((SKRect)(ref bounds)).Top, num2 - num, ((SKRect)(ref bounds)).Height, val); - } - finally - { - ((IDisposable)val)?.Dispose(); - } - } - - private void DrawCursor(SKCanvas canvas, SKPaint paint, string displayText, SKRect bounds) - { - //IL_0030: Unknown result type (might be due to invalid IL or missing references) - //IL_0035: Unknown result type (might be due to invalid IL or missing references) - //IL_0037: Unknown result type (might be due to invalid IL or missing references) - //IL_0041: Unknown result type (might be due to invalid IL or missing references) - //IL_004c: Unknown result type (might be due to invalid IL or missing references) - //IL_0054: Expected O, but got Unknown - string text = displayText.Substring(0, Math.Min(_cursorPosition, displayText.Length)); - float num = ((SKRect)(ref bounds)).Left - _scrollOffset + paint.MeasureText(text); - SKPaint val = new SKPaint - { - Color = CursorColor, - StrokeWidth = 2f, - IsAntialias = true - }; - try - { - canvas.DrawLine(num, ((SKRect)(ref bounds)).Top + 2f, num, ((SKRect)(ref bounds)).Bottom - 2f, val); - } - finally - { - ((IDisposable)val)?.Dispose(); - } - } - - private void ResetCursorBlink() - { - _cursorBlinkTime = DateTime.UtcNow; - _cursorVisible = true; - } - - public void UpdateCursorBlink() - { - if (base.IsFocused) - { - bool flag = (int)((DateTime.UtcNow - _cursorBlinkTime).TotalMilliseconds / 500.0) % 2 == 0; - if (flag != _cursorVisible) - { - _cursorVisible = flag; - Invalidate(); - } - } - } - - public override void OnTextInput(TextInputEventArgs e) - { - if (!base.IsEnabled || IsReadOnly || (!string.IsNullOrEmpty(e.Text) && e.Text.Length == 1 && e.Text[0] < ' ')) - { - return; - } - if (_selectionLength != 0) - { - DeleteSelection(); - } - if (MaxLength <= 0 || Text.Length < MaxLength) - { - string text = e.Text; - if (MaxLength > 0) - { - int val = MaxLength - Text.Length; - text = text.Substring(0, Math.Min(text.Length, val)); - } - string text2 = Text.Insert(_cursorPosition, text); - int cursorPosition = _cursorPosition; - Text = text2; - _cursorPosition = cursorPosition + text.Length; - ResetCursorBlink(); - Invalidate(); - } - } - - public override void OnKeyDown(KeyEventArgs e) - { - if (!base.IsEnabled) - { - return; - } - switch (e.Key) - { - case Key.Backspace: - if (!IsReadOnly) - { - if (_selectionLength > 0) - { - DeleteSelection(); - } - else if (_cursorPosition > 0) - { - string text = Text.Remove(_cursorPosition - 1, 1); - int cursorPosition = _cursorPosition - 1; - Text = text; - _cursorPosition = cursorPosition; - } - ResetCursorBlink(); - Invalidate(); - } - e.Handled = true; - break; - case Key.Delete: - if (!IsReadOnly) - { - if (_selectionLength > 0) - { - DeleteSelection(); - } - else if (_cursorPosition < Text.Length) - { - Text = Text.Remove(_cursorPosition, 1); - } - ResetCursorBlink(); - Invalidate(); - } - e.Handled = true; - break; - case Key.Left: - if (_cursorPosition > 0) - { - if (e.Modifiers.HasFlag(KeyModifiers.Shift)) - { - ExtendSelection(-1); - } - else - { - ClearSelection(); - _cursorPosition--; - } - ResetCursorBlink(); - Invalidate(); - } - e.Handled = true; - break; - case Key.Right: - if (_cursorPosition < Text.Length) - { - if (e.Modifiers.HasFlag(KeyModifiers.Shift)) - { - ExtendSelection(1); - } - else - { - ClearSelection(); - _cursorPosition++; - } - ResetCursorBlink(); - Invalidate(); - } - e.Handled = true; - break; - case Key.Home: - if (e.Modifiers.HasFlag(KeyModifiers.Shift)) - { - ExtendSelectionTo(0); - } - else - { - ClearSelection(); - _cursorPosition = 0; - } - ResetCursorBlink(); - Invalidate(); - e.Handled = true; - break; - case Key.End: - if (e.Modifiers.HasFlag(KeyModifiers.Shift)) - { - ExtendSelectionTo(Text.Length); - } - else - { - ClearSelection(); - _cursorPosition = Text.Length; - } - ResetCursorBlink(); - Invalidate(); - e.Handled = true; - break; - case Key.A: - if (e.Modifiers.HasFlag(KeyModifiers.Control)) - { - SelectAll(); - e.Handled = true; - } - break; - case Key.C: - if (e.Modifiers.HasFlag(KeyModifiers.Control)) - { - CopyToClipboard(); - e.Handled = true; - } - break; - case Key.V: - if (e.Modifiers.HasFlag(KeyModifiers.Control) && !IsReadOnly) - { - PasteFromClipboard(); - e.Handled = true; - } - break; - case Key.X: - if (e.Modifiers.HasFlag(KeyModifiers.Control) && !IsReadOnly) - { - CutToClipboard(); - e.Handled = true; - } - break; - case Key.Enter: - this.Completed?.Invoke(this, EventArgs.Empty); - e.Handled = true; - break; - } - } - - public override void OnPointerPressed(PointerEventArgs e) - { - //IL_014e: Unknown result type (might be due to invalid IL or missing references) - //IL_0153: Unknown result type (might be due to invalid IL or missing references) - //IL_0163: Unknown result type (might be due to invalid IL or missing references) - //IL_0168: Unknown result type (might be due to invalid IL or missing references) - //IL_00d1: Unknown result type (might be due to invalid IL or missing references) - //IL_00d6: Unknown result type (might be due to invalid IL or missing references) - //IL_00ee: Unknown result type (might be due to invalid IL or missing references) - //IL_00f3: Unknown result type (might be due to invalid IL or missing references) - Console.WriteLine($"[SkiaEntry] OnPointerPressed Button={e.Button} at ({e.X}, {e.Y})"); - if (!base.IsEnabled) - { - return; - } - if (e.Button == PointerButton.Right) - { - Console.WriteLine("[SkiaEntry] Right-click detected, showing context menu"); - ShowContextMenu(e.X, e.Y); - return; - } - SKRect val; - if (ShowClearButton && !string.IsNullOrEmpty(Text) && base.IsFocused) - { - float num = 20f; - float num2 = 8f; - val = base.Bounds; - float num3 = ((SKRect)(ref val)).Right - num2 - num / 2f; - val = base.Bounds; - float midY = ((SKRect)(ref val)).MidY; - float num4 = e.X - num3; - float num5 = e.Y - midY; - if (num4 * num4 + num5 * num5 < num / 2f * (num / 2f)) - { - Text = ""; - _cursorPosition = 0; - _selectionLength = 0; - Invalidate(); - return; - } - } - SKRect screenBounds = base.ScreenBounds; - float num6 = e.X - ((SKRect)(ref screenBounds)).Left; - val = Padding; - float x = num6 - ((SKRect)(ref val)).Left + _scrollOffset; - _cursorPosition = GetCharacterIndexAtX(x); - DateTime utcNow = DateTime.UtcNow; - double totalMilliseconds = (utcNow - _lastClickTime).TotalMilliseconds; - float num7 = Math.Abs(e.X - _lastClickX); - if (totalMilliseconds < 400.0 && num7 < 10f) - { - SelectWordAtCursor(); - _lastClickTime = DateTime.MinValue; - _isSelecting = false; - } - else - { - _selectionStart = _cursorPosition; - _selectionLength = 0; - _isSelecting = true; - _lastClickTime = utcNow; - _lastClickX = e.X; - } - ResetCursorBlink(); - Invalidate(); - } - - private void SelectWordAtCursor() - { - if (!string.IsNullOrEmpty(Text)) - { - int num = _cursorPosition; - int i = _cursorPosition; - while (num > 0 && IsWordChar(Text[num - 1])) - { - num--; - } - for (; i < Text.Length && IsWordChar(Text[i]); i++) - { - } - _selectionStart = num; - _cursorPosition = i; - _selectionLength = i - num; - } - } - - private static bool IsWordChar(char c) - { - if (!char.IsLetterOrDigit(c)) - { - return c == '_'; - } - return true; - } - - public override void OnPointerMoved(PointerEventArgs e) - { - //IL_0012: Unknown result type (might be due to invalid IL or missing references) - //IL_0017: Unknown result type (might be due to invalid IL or missing references) - //IL_0027: Unknown result type (might be due to invalid IL or missing references) - //IL_002c: Unknown result type (might be due to invalid IL or missing references) - if (base.IsEnabled && _isSelecting) - { - SKRect screenBounds = base.ScreenBounds; - float num = e.X - ((SKRect)(ref screenBounds)).Left; - SKRect padding = Padding; - float x = num - ((SKRect)(ref padding)).Left + _scrollOffset; - int characterIndexAtX = GetCharacterIndexAtX(x); - if (characterIndexAtX != _cursorPosition) - { - _cursorPosition = characterIndexAtX; - _selectionLength = _cursorPosition - _selectionStart; - ResetCursorBlink(); - Invalidate(); - } - } - } - - public override void OnPointerReleased(PointerEventArgs e) - { - _isSelecting = false; - } - - private int GetCharacterIndexAtX(float x) - { - //IL_004c: Unknown result type (might be due to invalid IL or missing references) - //IL_0052: Expected O, but got Unknown - //IL_0053: Unknown result type (might be due to invalid IL or missing references) - //IL_0059: Expected O, but got Unknown - if (string.IsNullOrEmpty(Text)) - { - return 0; - } - SKFontStyle fontStyle = GetFontStyle(); - SKFont val = new SKFont(SkiaRenderingEngine.Current?.ResourceCache.GetTypeface(FontFamily, fontStyle) ?? SKTypeface.Default, FontSize, 1f, 0f); - try - { - SKPaint val2 = new SKPaint(val); - try - { - string displayText = GetDisplayText(); - for (int i = 0; i <= displayText.Length; i++) - { - string text = displayText.Substring(0, i); - float num = val2.MeasureText(text); - if (!(num >= x)) - { - continue; - } - if (i > 0) - { - float num2 = val2.MeasureText(displayText.Substring(0, i - 1)); - if (x - num2 < num - x) - { - return i - 1; - } - } - return i; - } - return displayText.Length; - } - finally - { - ((IDisposable)val2)?.Dispose(); - } - } - finally - { - ((IDisposable)val)?.Dispose(); - } - } - - private void DeleteSelection() - { - int num = Math.Min(_selectionStart, _selectionStart + _selectionLength); - int count = Math.Abs(_selectionLength); - Text = Text.Remove(num, count); - _cursorPosition = num; - _selectionLength = 0; - } - - private void ClearSelection() - { - _selectionLength = 0; - } - - private void ExtendSelection(int delta) - { - if (_selectionLength == 0) - { - _selectionStart = _cursorPosition; - } - _cursorPosition += delta; - _selectionLength = _cursorPosition - _selectionStart; - } - - private void ExtendSelectionTo(int position) - { - if (_selectionLength == 0) - { - _selectionStart = _cursorPosition; - } - _cursorPosition = position; - _selectionLength = _cursorPosition - _selectionStart; - } - - public void SelectAll() - { - _selectionStart = 0; - _cursorPosition = Text.Length; - _selectionLength = Text.Length; - Invalidate(); - } - - private void CopyToClipboard() - { - if (!IsPassword && _selectionLength != 0) - { - int startIndex = Math.Min(_selectionStart, _selectionStart + _selectionLength); - int length = Math.Abs(_selectionLength); - SystemClipboard.SetText(Text.Substring(startIndex, length)); - } - } - - private void CutToClipboard() - { - if (!IsPassword) - { - CopyToClipboard(); - DeleteSelection(); - Invalidate(); - } - } - - private void PasteFromClipboard() - { - string text = SystemClipboard.GetText(); - if (!string.IsNullOrEmpty(text)) - { - if (_selectionLength != 0) - { - DeleteSelection(); - } - if (MaxLength > 0) - { - int val = MaxLength - Text.Length; - text = text.Substring(0, Math.Min(text.Length, val)); - } - string text2 = Text.Insert(_cursorPosition, text); - int cursorPosition = _cursorPosition + text.Length; - Text = text2; - _cursorPosition = cursorPosition; - Invalidate(); - } - } - - private void ShowContextMenu(float x, float y) - { - Console.WriteLine($"[SkiaEntry] ShowContextMenu at ({x}, {y})"); - bool isEnabled = _selectionLength != 0; - bool isEnabled2 = !string.IsNullOrEmpty(Text); - bool isEnabled3 = !string.IsNullOrEmpty(SystemClipboard.GetText()); - GtkContextMenuService.ShowContextMenu(new List - { - new GtkMenuItem("Cut", delegate - { - CutToClipboard(); - Invalidate(); - }, isEnabled), - new GtkMenuItem("Copy", delegate - { - CopyToClipboard(); - }, isEnabled), - new GtkMenuItem("Paste", delegate - { - PasteFromClipboard(); - Invalidate(); - }, isEnabled3), - GtkMenuItem.Separator, - new GtkMenuItem("Select All", delegate - { - SelectAll(); - Invalidate(); - }, isEnabled2) - }); - } - - public override void OnFocusGained() - { - base.OnFocusGained(); - SkiaVisualStateManager.GoToState(this, "Focused"); - } - - public override void OnFocusLost() - { - base.OnFocusLost(); - SkiaVisualStateManager.GoToState(this, "Normal"); - } - - protected override SKSize MeasureOverride(SKSize availableSize) - { - //IL_003d: Unknown result type (might be due to invalid IL or missing references) - //IL_0043: Expected O, but got Unknown - //IL_0044: Unknown result type (might be due to invalid IL or missing references) - //IL_0049: Unknown result type (might be due to invalid IL or missing references) - //IL_0069: Unknown result type (might be due to invalid IL or missing references) - //IL_006e: Unknown result type (might be due to invalid IL or missing references) - //IL_0079: Unknown result type (might be due to invalid IL or missing references) - //IL_007e: Unknown result type (might be due to invalid IL or missing references) - //IL_0095: Unknown result type (might be due to invalid IL or missing references) - //IL_009a: Unknown result type (might be due to invalid IL or missing references) - //IL_00a8: Unknown result type (might be due to invalid IL or missing references) - SKFontStyle fontStyle = GetFontStyle(); - SKFont val = new SKFont(SkiaRenderingEngine.Current?.ResourceCache.GetTypeface(FontFamily, fontStyle) ?? SKTypeface.Default, FontSize, 1f, 0f); - try - { - SKFontMetrics metrics = val.Metrics; - float num = ((SKFontMetrics)(ref metrics)).Descent - ((SKFontMetrics)(ref metrics)).Ascent + ((SKFontMetrics)(ref metrics)).Leading; - SKRect padding = Padding; - float num2 = num + ((SKRect)(ref padding)).Top; - padding = Padding; - return new SKSize(200f, num2 + ((SKRect)(ref padding)).Bottom + BorderWidth * 2f); - } - finally - { - ((IDisposable)val)?.Dispose(); - } - } + #region BindableProperties + + /// + /// Bindable property for Text. + /// + public static readonly BindableProperty TextProperty = + BindableProperty.Create( + nameof(Text), + typeof(string), + typeof(SkiaEntry), + "", + BindingMode.TwoWay, + propertyChanged: (b, o, n) => ((SkiaEntry)b).OnTextPropertyChanged((string)o, (string)n)); + + /// + /// Bindable property for Placeholder. + /// + public static readonly BindableProperty PlaceholderProperty = + BindableProperty.Create( + nameof(Placeholder), + typeof(string), + typeof(SkiaEntry), + "", + propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate()); + + /// + /// Bindable property for PlaceholderColor. + /// + public static readonly BindableProperty PlaceholderColorProperty = + BindableProperty.Create( + nameof(PlaceholderColor), + typeof(SKColor), + typeof(SkiaEntry), + new SKColor(0x9E, 0x9E, 0x9E), + propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate()); + + /// + /// Bindable property for TextColor. + /// + public static readonly BindableProperty TextColorProperty = + BindableProperty.Create( + nameof(TextColor), + typeof(SKColor), + typeof(SkiaEntry), + SKColors.Black, + propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate()); + + /// + /// Bindable property for EntryBackgroundColor. + /// + public static readonly BindableProperty EntryBackgroundColorProperty = + BindableProperty.Create( + nameof(EntryBackgroundColor), + typeof(SKColor), + typeof(SkiaEntry), + SKColors.White, + propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate()); + + /// + /// Bindable property for BorderColor. + /// + public static readonly BindableProperty BorderColorProperty = + BindableProperty.Create( + nameof(BorderColor), + typeof(SKColor), + typeof(SkiaEntry), + new SKColor(0xBD, 0xBD, 0xBD), + propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate()); + + /// + /// Bindable property for FocusedBorderColor. + /// + public static readonly BindableProperty FocusedBorderColorProperty = + BindableProperty.Create( + nameof(FocusedBorderColor), + typeof(SKColor), + typeof(SkiaEntry), + new SKColor(0x21, 0x96, 0xF3), + propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate()); + + /// + /// Bindable property for SelectionColor. + /// + public static readonly BindableProperty SelectionColorProperty = + BindableProperty.Create( + nameof(SelectionColor), + typeof(SKColor), + typeof(SkiaEntry), + new SKColor(0x21, 0x96, 0xF3, 0x80), + propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate()); + + /// + /// Bindable property for CursorColor. + /// + public static readonly BindableProperty CursorColorProperty = + BindableProperty.Create( + nameof(CursorColor), + typeof(SKColor), + typeof(SkiaEntry), + new SKColor(0x21, 0x96, 0xF3), + propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate()); + + /// + /// Bindable property for FontFamily. + /// + public static readonly BindableProperty FontFamilyProperty = + BindableProperty.Create( + nameof(FontFamily), + typeof(string), + typeof(SkiaEntry), + "Sans", + propertyChanged: (b, o, n) => ((SkiaEntry)b).InvalidateMeasure()); + + /// + /// Bindable property for FontSize. + /// + public static readonly BindableProperty FontSizeProperty = + BindableProperty.Create( + nameof(FontSize), + typeof(float), + typeof(SkiaEntry), + 14f, + propertyChanged: (b, o, n) => ((SkiaEntry)b).InvalidateMeasure()); + + /// + /// Bindable property for IsBold. + /// + public static readonly BindableProperty IsBoldProperty = + BindableProperty.Create( + nameof(IsBold), + typeof(bool), + typeof(SkiaEntry), + false, + propertyChanged: (b, o, n) => ((SkiaEntry)b).InvalidateMeasure()); + + /// + /// Bindable property for IsItalic. + /// + public static readonly BindableProperty IsItalicProperty = + BindableProperty.Create( + nameof(IsItalic), + typeof(bool), + typeof(SkiaEntry), + false, + propertyChanged: (b, o, n) => ((SkiaEntry)b).InvalidateMeasure()); + + /// + /// Bindable property for CornerRadius. + /// + public static readonly BindableProperty CornerRadiusProperty = + BindableProperty.Create( + nameof(CornerRadius), + typeof(float), + typeof(SkiaEntry), + 4f, + propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate()); + + /// + /// Bindable property for BorderWidth. + /// + public static readonly BindableProperty BorderWidthProperty = + BindableProperty.Create( + nameof(BorderWidth), + typeof(float), + typeof(SkiaEntry), + 1f, + propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate()); + + /// + /// Bindable property for Padding. + /// + public static readonly BindableProperty PaddingProperty = + BindableProperty.Create( + nameof(Padding), + typeof(SKRect), + typeof(SkiaEntry), + new SKRect(12, 8, 12, 8), + propertyChanged: (b, o, n) => ((SkiaEntry)b).InvalidateMeasure()); + + /// + /// Bindable property for IsPassword. + /// + public static readonly BindableProperty IsPasswordProperty = + BindableProperty.Create( + nameof(IsPassword), + typeof(bool), + typeof(SkiaEntry), + false, + propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate()); + + /// + /// Bindable property for PasswordChar. + /// + public static readonly BindableProperty PasswordCharProperty = + BindableProperty.Create( + nameof(PasswordChar), + typeof(char), + typeof(SkiaEntry), + '*', // Use asterisk for universal font compatibility + propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate()); + + /// + /// Bindable property for MaxLength. + /// + public static readonly BindableProperty MaxLengthProperty = + BindableProperty.Create( + nameof(MaxLength), + typeof(int), + typeof(SkiaEntry), + 0); + + /// + /// Bindable property for IsReadOnly. + /// + public static readonly BindableProperty IsReadOnlyProperty = + BindableProperty.Create( + nameof(IsReadOnly), + typeof(bool), + typeof(SkiaEntry), + false, + propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate()); + + /// + /// Bindable property for HorizontalTextAlignment. + /// + public static readonly BindableProperty HorizontalTextAlignmentProperty = + BindableProperty.Create( + nameof(HorizontalTextAlignment), + typeof(TextAlignment), + typeof(SkiaEntry), + TextAlignment.Start, + propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate()); + + /// + /// Bindable property for VerticalTextAlignment. + /// + public static readonly BindableProperty VerticalTextAlignmentProperty = + BindableProperty.Create( + nameof(VerticalTextAlignment), + typeof(TextAlignment), + typeof(SkiaEntry), + TextAlignment.Center, + propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate()); + + /// + /// Bindable property for ShowClearButton. + /// + public static readonly BindableProperty ShowClearButtonProperty = + BindableProperty.Create( + nameof(ShowClearButton), + typeof(bool), + typeof(SkiaEntry), + false, + propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate()); + + /// + /// Bindable property for CharacterSpacing. + /// + public static readonly BindableProperty CharacterSpacingProperty = + BindableProperty.Create( + nameof(CharacterSpacing), + typeof(float), + typeof(SkiaEntry), + 0f, + propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate()); + + #endregion + + #region Properties + + /// + /// Gets or sets the text content. + /// + public string Text + { + get => (string)GetValue(TextProperty); + set => SetValue(TextProperty, value); + } + + /// + /// Gets or sets the placeholder text. + /// + public string Placeholder + { + get => (string)GetValue(PlaceholderProperty); + set => SetValue(PlaceholderProperty, value); + } + + /// + /// Gets or sets the placeholder color. + /// + public SKColor PlaceholderColor + { + get => (SKColor)GetValue(PlaceholderColorProperty); + set => SetValue(PlaceholderColorProperty, value); + } + + /// + /// Gets or sets the text color. + /// + public SKColor TextColor + { + get => (SKColor)GetValue(TextColorProperty); + set => SetValue(TextColorProperty, value); + } + + /// + /// Gets or sets the entry background color. + /// + public SKColor EntryBackgroundColor + { + get => (SKColor)GetValue(EntryBackgroundColorProperty); + set => SetValue(EntryBackgroundColorProperty, value); + } + + /// + /// Gets or sets the border color. + /// + public SKColor BorderColor + { + get => (SKColor)GetValue(BorderColorProperty); + set => SetValue(BorderColorProperty, value); + } + + /// + /// Gets or sets the focused border color. + /// + public SKColor FocusedBorderColor + { + get => (SKColor)GetValue(FocusedBorderColorProperty); + set => SetValue(FocusedBorderColorProperty, value); + } + + /// + /// Gets or sets the selection color. + /// + public SKColor SelectionColor + { + get => (SKColor)GetValue(SelectionColorProperty); + set => SetValue(SelectionColorProperty, value); + } + + /// + /// Gets or sets the cursor color. + /// + public SKColor CursorColor + { + get => (SKColor)GetValue(CursorColorProperty); + set => SetValue(CursorColorProperty, value); + } + + /// + /// Gets or sets the font family. + /// + public string FontFamily + { + get => (string)GetValue(FontFamilyProperty); + set => SetValue(FontFamilyProperty, value); + } + + /// + /// Gets or sets the font size. + /// + public float FontSize + { + get => (float)GetValue(FontSizeProperty); + set => SetValue(FontSizeProperty, value); + } + + /// + /// Gets or sets whether the text is bold. + /// + public bool IsBold + { + get => (bool)GetValue(IsBoldProperty); + set => SetValue(IsBoldProperty, value); + } + + /// + /// Gets or sets whether the text is italic. + /// + public bool IsItalic + { + get => (bool)GetValue(IsItalicProperty); + set => SetValue(IsItalicProperty, value); + } + + /// + /// Gets or sets the corner radius. + /// + public float CornerRadius + { + get => (float)GetValue(CornerRadiusProperty); + set => SetValue(CornerRadiusProperty, value); + } + + /// + /// Gets or sets the border width. + /// + public float BorderWidth + { + get => (float)GetValue(BorderWidthProperty); + set => SetValue(BorderWidthProperty, value); + } + + /// + /// Gets or sets the padding. + /// + public SKRect Padding + { + get => (SKRect)GetValue(PaddingProperty); + set => SetValue(PaddingProperty, value); + } + + /// + /// Gets or sets whether this is a password field. + /// + public bool IsPassword + { + get => (bool)GetValue(IsPasswordProperty); + set => SetValue(IsPasswordProperty, value); + } + + /// + /// Gets or sets the password masking character. + /// + public char PasswordChar + { + get => (char)GetValue(PasswordCharProperty); + set => SetValue(PasswordCharProperty, value); + } + + /// + /// Gets or sets the maximum text length. 0 = unlimited. + /// + public int MaxLength + { + get => (int)GetValue(MaxLengthProperty); + set => SetValue(MaxLengthProperty, value); + } + + /// + /// Gets or sets whether the entry is read-only. + /// + public bool IsReadOnly + { + get => (bool)GetValue(IsReadOnlyProperty); + set => SetValue(IsReadOnlyProperty, value); + } + + /// + /// Gets or sets the horizontal text alignment. + /// + public TextAlignment HorizontalTextAlignment + { + get => (TextAlignment)GetValue(HorizontalTextAlignmentProperty); + set => SetValue(HorizontalTextAlignmentProperty, value); + } + + /// + /// Gets or sets the vertical text alignment. + /// + public TextAlignment VerticalTextAlignment + { + get => (TextAlignment)GetValue(VerticalTextAlignmentProperty); + set => SetValue(VerticalTextAlignmentProperty, value); + } + + /// + /// Gets or sets whether to show the clear button. + /// + public bool ShowClearButton + { + get => (bool)GetValue(ShowClearButtonProperty); + set => SetValue(ShowClearButtonProperty, value); + } + + /// + /// Gets or sets the character spacing. + /// + public float CharacterSpacing + { + get => (float)GetValue(CharacterSpacingProperty); + set => SetValue(CharacterSpacingProperty, value); + } + + /// + /// Gets or sets the cursor position. + /// + public int CursorPosition + { + get => _cursorPosition; + set + { + _cursorPosition = Math.Clamp(value, 0, Text.Length); + ResetCursorBlink(); + Invalidate(); + } + } + + /// + /// Gets or sets the selection length. + /// + public int SelectionLength + { + get => _selectionLength; + set + { + _selectionLength = value; + Invalidate(); + } + } + + #endregion + + private int _cursorPosition; + private int _selectionStart; + private int _selectionLength; + private float _scrollOffset; + private DateTime _cursorBlinkTime = DateTime.UtcNow; + private bool _cursorVisible = true; + private bool _isSelecting; // For mouse-based text selection + private DateTime _lastClickTime = DateTime.MinValue; + private float _lastClickX; + private const double DoubleClickThresholdMs = 400; + + /// + /// Event raised when text changes. + /// + public event EventHandler? TextChanged; + + /// + /// Event raised when Enter is pressed. + /// + public event EventHandler? Completed; + + public SkiaEntry() + { + IsFocusable = true; + } + + private void OnTextPropertyChanged(string oldText, string newText) + { + _cursorPosition = Math.Min(_cursorPosition, (newText ?? "").Length); + _scrollOffset = 0; // Reset scroll when text changes externally + _selectionLength = 0; + TextChanged?.Invoke(this, new TextChangedEventArgs(oldText, newText ?? "")); + Invalidate(); + } + + protected override void OnDraw(SKCanvas canvas, SKRect bounds) + { + // Draw background + using var bgPaint = new SKPaint + { + Color = EntryBackgroundColor, + IsAntialias = true, + Style = SKPaintStyle.Fill + }; + + var rect = new SKRoundRect(bounds, CornerRadius); + canvas.DrawRoundRect(rect, bgPaint); + + // Draw border + var borderColor = IsFocused ? FocusedBorderColor : BorderColor; + var borderWidth = IsFocused ? BorderWidth + 1 : BorderWidth; + + 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( + bounds.Left + Padding.Left, + bounds.Top + Padding.Top, + bounds.Right - Padding.Right, + bounds.Bottom - Padding.Bottom); + + // Reserve space for clear button if shown + var clearButtonSize = 20f; + var clearButtonMargin = 8f; + if (ShowClearButton && !string.IsNullOrEmpty(Text) && IsFocused) + { + contentBounds.Right -= clearButtonSize + clearButtonMargin; + } + + // Set up clipping for text area + canvas.Save(); + canvas.ClipRect(contentBounds); + + var fontStyle = GetFontStyle(); + var typeface = SkiaRenderingEngine.Current?.ResourceCache.GetTypeface(FontFamily, fontStyle) + ?? SKTypeface.Default; + + using var font = new SKFont(typeface, FontSize); + using var paint = new SKPaint(font) { IsAntialias = true }; + + var displayText = GetDisplayText(); + var hasText = !string.IsNullOrEmpty(displayText); + + if (hasText) + { + paint.Color = TextColor; + + // Measure text to cursor position for scrolling + var textToCursor = displayText.Substring(0, Math.Min(_cursorPosition, displayText.Length)); + var cursorX = paint.MeasureText(textToCursor); + + // Auto-scroll to keep cursor visible + if (cursorX - _scrollOffset > contentBounds.Width - 10) + { + _scrollOffset = cursorX - contentBounds.Width + 10; + } + else if (cursorX - _scrollOffset < 0) + { + _scrollOffset = cursorX; + } + + // Draw selection (check != 0 to handle both forward and backward selection) + if (IsFocused && _selectionLength != 0) + { + DrawSelection(canvas, paint, displayText, contentBounds); + } + + // Calculate text position based on vertical alignment + var textBounds = new SKRect(); + paint.MeasureText(displayText, ref textBounds); + + float x = contentBounds.Left - _scrollOffset; + float y = VerticalTextAlignment switch + { + TextAlignment.Start => contentBounds.Top - textBounds.Top, + TextAlignment.End => contentBounds.Bottom - textBounds.Bottom, + _ => contentBounds.MidY - textBounds.MidY // Center + }; + + canvas.DrawText(displayText, x, y, paint); + + // Draw cursor + if (IsFocused && !IsReadOnly && _cursorVisible) + { + DrawCursor(canvas, paint, displayText, contentBounds); + } + } + else if (!string.IsNullOrEmpty(Placeholder)) + { + // Draw placeholder + paint.Color = PlaceholderColor; + + var textBounds = new SKRect(); + paint.MeasureText(Placeholder, ref textBounds); + + float x = contentBounds.Left; + float y = contentBounds.MidY - textBounds.MidY; + + canvas.DrawText(Placeholder, x, y, paint); + } + else if (IsFocused && !IsReadOnly && _cursorVisible) + { + // Draw cursor even with no text + DrawCursor(canvas, paint, "", contentBounds); + } + + canvas.Restore(); + + // Draw clear button if applicable + if (ShowClearButton && !string.IsNullOrEmpty(Text) && IsFocused) + { + DrawClearButton(canvas, bounds, clearButtonSize, clearButtonMargin); + } + } + + private SKFontStyle GetFontStyle() + { + if (IsBold && IsItalic) + return SKFontStyle.BoldItalic; + if (IsBold) + return SKFontStyle.Bold; + if (IsItalic) + return SKFontStyle.Italic; + return SKFontStyle.Normal; + } + + private void DrawClearButton(SKCanvas canvas, SKRect bounds, float size, float margin) + { + var centerX = bounds.Right - margin - size / 2; + var centerY = bounds.MidY; + + // Draw circle background + using var circlePaint = new SKPaint + { + Color = new SKColor(0xBD, 0xBD, 0xBD), + IsAntialias = true, + Style = SKPaintStyle.Fill + }; + canvas.DrawCircle(centerX, centerY, size / 2 - 2, circlePaint); + + // Draw X + using var xPaint = new SKPaint + { + Color = SKColors.White, + IsAntialias = true, + Style = SKPaintStyle.Stroke, + StrokeWidth = 2, + StrokeCap = SKStrokeCap.Round + }; + + var offset = size / 4 - 1; + canvas.DrawLine(centerX - offset, centerY - offset, centerX + offset, centerY + offset, xPaint); + canvas.DrawLine(centerX - offset, centerY + offset, centerX + offset, centerY - offset, xPaint); + } + + private string GetDisplayText() + { + if (IsPassword && !string.IsNullOrEmpty(Text)) + { + return new string(PasswordChar, Text.Length); + } + return Text; + } + + private void DrawSelection(SKCanvas canvas, SKPaint paint, string displayText, SKRect bounds) + { + var selStart = Math.Min(_selectionStart, _selectionStart + _selectionLength); + var selEnd = Math.Max(_selectionStart, _selectionStart + _selectionLength); + + var textToStart = displayText.Substring(0, selStart); + var textToEnd = displayText.Substring(0, selEnd); + + var startX = bounds.Left - _scrollOffset + paint.MeasureText(textToStart); + var endX = bounds.Left - _scrollOffset + paint.MeasureText(textToEnd); + + using var selPaint = new SKPaint + { + Color = SelectionColor, + Style = SKPaintStyle.Fill + }; + + canvas.DrawRect(startX, bounds.Top, endX - startX, bounds.Height, selPaint); + } + + private void DrawCursor(SKCanvas canvas, SKPaint paint, string displayText, SKRect bounds) + { + var textToCursor = displayText.Substring(0, Math.Min(_cursorPosition, displayText.Length)); + var cursorX = bounds.Left - _scrollOffset + paint.MeasureText(textToCursor); + + using var cursorPaint = new SKPaint + { + Color = CursorColor, + StrokeWidth = 2, + IsAntialias = true + }; + + canvas.DrawLine(cursorX, bounds.Top + 2, cursorX, bounds.Bottom - 2, cursorPaint); + } + + private void ResetCursorBlink() + { + _cursorBlinkTime = DateTime.UtcNow; + _cursorVisible = true; + } + + /// + /// Updates cursor blink animation. + /// + public void UpdateCursorBlink() + { + if (!IsFocused) return; + + var elapsed = (DateTime.UtcNow - _cursorBlinkTime).TotalMilliseconds; + var newVisible = ((int)(elapsed / 500) % 2) == 0; + + if (newVisible != _cursorVisible) + { + _cursorVisible = newVisible; + Invalidate(); + } + } + + public override void OnTextInput(TextInputEventArgs e) + { + if (!IsEnabled || IsReadOnly) return; + + // Ignore control characters (Ctrl+key combinations send ASCII control codes) + if (!string.IsNullOrEmpty(e.Text) && e.Text.Length == 1 && e.Text[0] < 32) + return; + + // Delete selection if any + if (_selectionLength != 0) + { + DeleteSelection(); + } + + // Check max length + if (MaxLength > 0 && Text.Length >= MaxLength) + return; + + // Insert text at cursor + var insertText = e.Text; + if (MaxLength > 0) + { + var remaining = MaxLength - Text.Length; + insertText = insertText.Substring(0, Math.Min(insertText.Length, remaining)); + } + + var newText = Text.Insert(_cursorPosition, insertText); + var oldPos = _cursorPosition; + Text = newText; + _cursorPosition = oldPos + insertText.Length; + + ResetCursorBlink(); + Invalidate(); + } + + public override void OnKeyDown(KeyEventArgs e) + { + if (!IsEnabled) return; + + switch (e.Key) + { + case Key.Backspace: + if (!IsReadOnly) + { + if (_selectionLength > 0) + { + DeleteSelection(); + } + else if (_cursorPosition > 0) + { + var newText = Text.Remove(_cursorPosition - 1, 1); + var newPos = _cursorPosition - 1; + Text = newText; + _cursorPosition = newPos; + } + ResetCursorBlink(); + Invalidate(); + } + e.Handled = true; + break; + + case Key.Delete: + if (!IsReadOnly) + { + if (_selectionLength > 0) + { + DeleteSelection(); + } + else if (_cursorPosition < Text.Length) + { + Text = Text.Remove(_cursorPosition, 1); + } + ResetCursorBlink(); + Invalidate(); + } + e.Handled = true; + break; + + case Key.Left: + if (_cursorPosition > 0) + { + if (e.Modifiers.HasFlag(KeyModifiers.Shift)) + { + ExtendSelection(-1); + } + else + { + ClearSelection(); + _cursorPosition--; + } + ResetCursorBlink(); + Invalidate(); + } + e.Handled = true; + break; + + case Key.Right: + if (_cursorPosition < Text.Length) + { + if (e.Modifiers.HasFlag(KeyModifiers.Shift)) + { + ExtendSelection(1); + } + else + { + ClearSelection(); + _cursorPosition++; + } + ResetCursorBlink(); + Invalidate(); + } + e.Handled = true; + break; + + case Key.Home: + if (e.Modifiers.HasFlag(KeyModifiers.Shift)) + { + ExtendSelectionTo(0); + } + else + { + ClearSelection(); + _cursorPosition = 0; + } + ResetCursorBlink(); + Invalidate(); + e.Handled = true; + break; + + case Key.End: + if (e.Modifiers.HasFlag(KeyModifiers.Shift)) + { + ExtendSelectionTo(Text.Length); + } + else + { + ClearSelection(); + _cursorPosition = Text.Length; + } + ResetCursorBlink(); + Invalidate(); + e.Handled = true; + break; + + case Key.A: + if (e.Modifiers.HasFlag(KeyModifiers.Control)) + { + SelectAll(); + e.Handled = true; + } + break; + + case Key.C: + if (e.Modifiers.HasFlag(KeyModifiers.Control)) + { + CopyToClipboard(); + e.Handled = true; + } + break; + + case Key.V: + if (e.Modifiers.HasFlag(KeyModifiers.Control) && !IsReadOnly) + { + PasteFromClipboard(); + e.Handled = true; + } + break; + + case Key.X: + if (e.Modifiers.HasFlag(KeyModifiers.Control) && !IsReadOnly) + { + CutToClipboard(); + e.Handled = true; + } + break; + + case Key.Enter: + Completed?.Invoke(this, EventArgs.Empty); + e.Handled = true; + break; + } + } + + public override void OnPointerPressed(PointerEventArgs e) + { + Console.WriteLine($"[SkiaEntry] OnPointerPressed - Text='{Text}', Placeholder='{Placeholder}', IsEnabled={IsEnabled}, IsFocused={IsFocused}"); + Console.WriteLine($"[SkiaEntry] Bounds={Bounds}, ScreenBounds={ScreenBounds}, e.X={e.X}, e.Y={e.Y}"); + + if (!IsEnabled) return; + + // Check if clicked on clear button + if (ShowClearButton && !string.IsNullOrEmpty(Text) && IsFocused) + { + var clearButtonSize = 20f; + var clearButtonMargin = 8f; + var clearCenterX = Bounds.Right - clearButtonMargin - clearButtonSize / 2; + var clearCenterY = Bounds.MidY; + + var dx = e.X - clearCenterX; + var dy = e.Y - clearCenterY; + if (dx * dx + dy * dy < (clearButtonSize / 2) * (clearButtonSize / 2)) + { + // Clear button clicked + Text = ""; + _cursorPosition = 0; + _selectionLength = 0; + Invalidate(); + return; + } + } + + // Calculate cursor position from click using screen coordinates + var screenBounds = ScreenBounds; + var clickX = e.X - screenBounds.Left - Padding.Left + _scrollOffset; + _cursorPosition = GetCharacterIndexAtX(clickX); + + // Check for double-click (select word) + var now = DateTime.UtcNow; + var timeSinceLastClick = (now - _lastClickTime).TotalMilliseconds; + var distanceFromLastClick = Math.Abs(e.X - _lastClickX); + + if (timeSinceLastClick < DoubleClickThresholdMs && distanceFromLastClick < 10) + { + // Double-click: select the word at cursor + SelectWordAtCursor(); + _lastClickTime = DateTime.MinValue; // Reset to prevent triple-click issues + _isSelecting = false; + } + else + { + // Single click: start selection + _selectionStart = _cursorPosition; + _selectionLength = 0; + _isSelecting = true; + _lastClickTime = now; + _lastClickX = e.X; + } + + ResetCursorBlink(); + Invalidate(); + } + + private void SelectWordAtCursor() + { + if (string.IsNullOrEmpty(Text)) return; + + // Find word boundaries + int start = _cursorPosition; + int end = _cursorPosition; + + // Move start backwards to beginning of word + while (start > 0 && IsWordChar(Text[start - 1])) + start--; + + // Move end forwards to end of word + while (end < Text.Length && IsWordChar(Text[end])) + end++; + + _selectionStart = start; + _cursorPosition = end; + _selectionLength = end - start; + } + + private static bool IsWordChar(char c) + { + return char.IsLetterOrDigit(c) || c == '_'; + } + + public override void OnPointerMoved(PointerEventArgs e) + { + if (!IsEnabled || !_isSelecting) return; + + // Extend selection to current mouse position + var screenBounds = ScreenBounds; + var clickX = e.X - screenBounds.Left - Padding.Left + _scrollOffset; + var newPosition = GetCharacterIndexAtX(clickX); + + if (newPosition != _cursorPosition) + { + _cursorPosition = newPosition; + _selectionLength = _cursorPosition - _selectionStart; + ResetCursorBlink(); + Invalidate(); + } + } + + public override void OnPointerReleased(PointerEventArgs e) + { + _isSelecting = false; + } + + private int GetCharacterIndexAtX(float x) + { + if (string.IsNullOrEmpty(Text)) return 0; + + var fontStyle = GetFontStyle(); + var typeface = SkiaRenderingEngine.Current?.ResourceCache.GetTypeface(FontFamily, fontStyle) + ?? SKTypeface.Default; + + using var font = new SKFont(typeface, FontSize); + using var paint = new SKPaint(font); + + var displayText = GetDisplayText(); + + for (int i = 0; i <= displayText.Length; i++) + { + var substring = displayText.Substring(0, i); + var width = paint.MeasureText(substring); + + if (width >= x) + { + // Check if closer to current or previous character + if (i > 0) + { + var prevWidth = paint.MeasureText(displayText.Substring(0, i - 1)); + if (x - prevWidth < width - x) + return i - 1; + } + return i; + } + } + + return displayText.Length; + } + + private void DeleteSelection() + { + var start = Math.Min(_selectionStart, _selectionStart + _selectionLength); + var length = Math.Abs(_selectionLength); + + Text = Text.Remove(start, length); + _cursorPosition = start; + _selectionLength = 0; + } + + private void ClearSelection() + { + _selectionLength = 0; + } + + private void ExtendSelection(int delta) + { + if (_selectionLength == 0) + { + _selectionStart = _cursorPosition; + } + + _cursorPosition += delta; + _selectionLength = _cursorPosition - _selectionStart; + } + + private void ExtendSelectionTo(int position) + { + if (_selectionLength == 0) + { + _selectionStart = _cursorPosition; + } + + _cursorPosition = position; + _selectionLength = _cursorPosition - _selectionStart; + } + + /// + /// Selects all text. + /// + public void SelectAll() + { + _selectionStart = 0; + _cursorPosition = Text.Length; + _selectionLength = Text.Length; + Invalidate(); + } + + private void CopyToClipboard() + { + // Password fields should not allow copying + if (IsPassword) return; + if (_selectionLength == 0) return; + + var start = Math.Min(_selectionStart, _selectionStart + _selectionLength); + var length = Math.Abs(_selectionLength); + var selectedText = Text.Substring(start, length); + + // Use system clipboard via xclip/xsel + SystemClipboard.SetText(selectedText); + } + + private void CutToClipboard() + { + // Password fields should not allow cutting + if (IsPassword) return; + + CopyToClipboard(); + DeleteSelection(); + Invalidate(); + } + + private void PasteFromClipboard() + { + // Get from system clipboard + var text = SystemClipboard.GetText(); + if (string.IsNullOrEmpty(text)) return; + + if (_selectionLength != 0) + { + DeleteSelection(); + } + + // Check max length + if (MaxLength > 0) + { + var remaining = MaxLength - Text.Length; + text = text.Substring(0, Math.Min(text.Length, remaining)); + } + + var newText = Text.Insert(_cursorPosition, text); + var newPos = _cursorPosition + text.Length; + Text = newText; + _cursorPosition = newPos; + Invalidate(); + } + + public override void OnFocusGained() + { + base.OnFocusGained(); + SkiaVisualStateManager.GoToState(this, SkiaVisualStateManager.CommonStates.Focused); + } + + public override void OnFocusLost() + { + base.OnFocusLost(); + SkiaVisualStateManager.GoToState(this, SkiaVisualStateManager.CommonStates.Normal); + } + + protected override SKSize MeasureOverride(SKSize availableSize) + { + var fontStyle = GetFontStyle(); + var typeface = SkiaRenderingEngine.Current?.ResourceCache.GetTypeface(FontFamily, fontStyle) + ?? SKTypeface.Default; + + using var font = new SKFont(typeface, FontSize); + + // Use font metrics for consistent height regardless of text content + // This prevents size changes when placeholder disappears or text changes + var metrics = font.Metrics; + var textHeight = metrics.Descent - metrics.Ascent + metrics.Leading; + + return new SKSize( + 200, // Default width, will be overridden by layout + textHeight + Padding.Top + Padding.Bottom + BorderWidth * 2); + } +} + +/// +/// Event args for text changed events. +/// +public class TextChangedEventArgs : EventArgs +{ + public string OldTextValue { get; } + public string NewTextValue { get; } + + public TextChangedEventArgs(string oldText, string newText) + { + OldTextValue = oldText; + NewTextValue = newText; + } } diff --git a/Views/SkiaFlexLayout.cs b/Views/SkiaFlexLayout.cs deleted file mode 100644 index 9564b58..0000000 --- a/Views/SkiaFlexLayout.cs +++ /dev/null @@ -1,346 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.Maui.Controls; -using SkiaSharp; - -namespace Microsoft.Maui.Platform; - -public class SkiaFlexLayout : SkiaLayoutView -{ - public static readonly BindableProperty DirectionProperty = BindableProperty.Create("Direction", typeof(FlexDirection), typeof(SkiaFlexLayout), (object)FlexDirection.Row, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaFlexLayout)(object)b).InvalidateMeasure(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty WrapProperty = BindableProperty.Create("Wrap", typeof(FlexWrap), typeof(SkiaFlexLayout), (object)FlexWrap.NoWrap, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaFlexLayout)(object)b).InvalidateMeasure(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty JustifyContentProperty = BindableProperty.Create("JustifyContent", typeof(FlexJustify), typeof(SkiaFlexLayout), (object)FlexJustify.Start, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaFlexLayout)(object)b).InvalidateMeasure(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty AlignItemsProperty = BindableProperty.Create("AlignItems", typeof(FlexAlignItems), typeof(SkiaFlexLayout), (object)FlexAlignItems.Stretch, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaFlexLayout)(object)b).InvalidateMeasure(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty AlignContentProperty = BindableProperty.Create("AlignContent", typeof(FlexAlignContent), typeof(SkiaFlexLayout), (object)FlexAlignContent.Stretch, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaFlexLayout)(object)b).InvalidateMeasure(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty OrderProperty = BindableProperty.CreateAttached("Order", typeof(int), typeof(SkiaFlexLayout), (object)0, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)null, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty GrowProperty = BindableProperty.CreateAttached("Grow", typeof(float), typeof(SkiaFlexLayout), (object)0f, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)null, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty ShrinkProperty = BindableProperty.CreateAttached("Shrink", typeof(float), typeof(SkiaFlexLayout), (object)1f, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)null, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty BasisProperty = BindableProperty.CreateAttached("Basis", typeof(FlexBasis), typeof(SkiaFlexLayout), (object)FlexBasis.Auto, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)null, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty AlignSelfProperty = BindableProperty.CreateAttached("AlignSelf", typeof(FlexAlignSelf), typeof(SkiaFlexLayout), (object)FlexAlignSelf.Auto, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)null, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public FlexDirection Direction - { - get - { - return (FlexDirection)((BindableObject)this).GetValue(DirectionProperty); - } - set - { - ((BindableObject)this).SetValue(DirectionProperty, (object)value); - } - } - - public FlexWrap Wrap - { - get - { - return (FlexWrap)((BindableObject)this).GetValue(WrapProperty); - } - set - { - ((BindableObject)this).SetValue(WrapProperty, (object)value); - } - } - - public FlexJustify JustifyContent - { - get - { - return (FlexJustify)((BindableObject)this).GetValue(JustifyContentProperty); - } - set - { - ((BindableObject)this).SetValue(JustifyContentProperty, (object)value); - } - } - - public FlexAlignItems AlignItems - { - get - { - return (FlexAlignItems)((BindableObject)this).GetValue(AlignItemsProperty); - } - set - { - ((BindableObject)this).SetValue(AlignItemsProperty, (object)value); - } - } - - public FlexAlignContent AlignContent - { - get - { - return (FlexAlignContent)((BindableObject)this).GetValue(AlignContentProperty); - } - set - { - ((BindableObject)this).SetValue(AlignContentProperty, (object)value); - } - } - - public static int GetOrder(SkiaView view) - { - return (int)((BindableObject)view).GetValue(OrderProperty); - } - - public static void SetOrder(SkiaView view, int value) - { - ((BindableObject)view).SetValue(OrderProperty, (object)value); - } - - public static float GetGrow(SkiaView view) - { - return (float)((BindableObject)view).GetValue(GrowProperty); - } - - public static void SetGrow(SkiaView view, float value) - { - ((BindableObject)view).SetValue(GrowProperty, (object)value); - } - - public static float GetShrink(SkiaView view) - { - return (float)((BindableObject)view).GetValue(ShrinkProperty); - } - - public static void SetShrink(SkiaView view, float value) - { - ((BindableObject)view).SetValue(ShrinkProperty, (object)value); - } - - public static FlexBasis GetBasis(SkiaView view) - { - return (FlexBasis)((BindableObject)view).GetValue(BasisProperty); - } - - public static void SetBasis(SkiaView view, FlexBasis value) - { - ((BindableObject)view).SetValue(BasisProperty, (object)value); - } - - public static FlexAlignSelf GetAlignSelf(SkiaView view) - { - return (FlexAlignSelf)((BindableObject)view).GetValue(AlignSelfProperty); - } - - public static void SetAlignSelf(SkiaView view, FlexAlignSelf value) - { - ((BindableObject)view).SetValue(AlignSelfProperty, (object)value); - } - - protected override SKSize MeasureOverride(SKSize availableSize) - { - //IL_0042: Unknown result type (might be due to invalid IL or missing references) - //IL_0043: Unknown result type (might be due to invalid IL or missing references) - //IL_0048: Unknown result type (might be due to invalid IL or missing references) - //IL_00a0: Unknown result type (might be due to invalid IL or missing references) - //IL_0098: Unknown result type (might be due to invalid IL or missing references) - bool flag = Direction == FlexDirection.Row || Direction == FlexDirection.RowReverse; - float num = 0f; - float num2 = 0f; - foreach (SkiaView child in base.Children) - { - if (child.IsVisible) - { - SKSize val = child.Measure(availableSize); - if (flag) - { - num += ((SKSize)(ref val)).Width; - num2 = Math.Max(num2, ((SKSize)(ref val)).Height); - } - else - { - num += ((SKSize)(ref val)).Height; - num2 = Math.Max(num2, ((SKSize)(ref val)).Width); - } - } - } - if (!flag) - { - return new SKSize(num2, num); - } - return new SKSize(num, num2); - } - - protected override SKRect ArrangeOverride(SKRect bounds) - { - //IL_000d: Unknown result type (might be due to invalid IL or missing references) - //IL_0096: Unknown result type (might be due to invalid IL or missing references) - //IL_0127: Unknown result type (might be due to invalid IL or missing references) - //IL_012c: Unknown result type (might be due to invalid IL or missing references) - //IL_0131: Unknown result type (might be due to invalid IL or missing references) - //IL_0163: Unknown result type (might be due to invalid IL or missing references) - //IL_0168: Unknown result type (might be due to invalid IL or missing references) - //IL_014c: Unknown result type (might be due to invalid IL or missing references) - //IL_0151: Unknown result type (might be due to invalid IL or missing references) - //IL_0173: Unknown result type (might be due to invalid IL or missing references) - //IL_016d: Unknown result type (might be due to invalid IL or missing references) - //IL_01f0: Unknown result type (might be due to invalid IL or missing references) - //IL_01f5: Unknown result type (might be due to invalid IL or missing references) - //IL_04b4: Unknown result type (might be due to invalid IL or missing references) - //IL_0489: Unknown result type (might be due to invalid IL or missing references) - if (base.Children.Count == 0) - { - return bounds; - } - bool flag = Direction == FlexDirection.Row || Direction == FlexDirection.RowReverse; - bool flag2 = Direction == FlexDirection.RowReverse || Direction == FlexDirection.ColumnReverse; - List list = (from c in base.Children - where c.IsVisible - orderby GetOrder(c) - select c).ToList(); - if (list.Count == 0) - { - return bounds; - } - float num = (flag ? ((SKRect)(ref bounds)).Width : ((SKRect)(ref bounds)).Height); - float num2 = (flag ? ((SKRect)(ref bounds)).Height : ((SKRect)(ref bounds)).Width); - List<(SkiaView, SKSize, float, float)> list2 = new List<(SkiaView, SKSize, float, float)>(); - float num3 = 0f; - float num4 = 0f; - float num5 = 0f; - foreach (SkiaView item10 in list) - { - FlexBasis basis = GetBasis(item10); - float grow = GetGrow(item10); - float shrink = GetShrink(item10); - SKSize item; - if (basis.IsAuto) - { - item = item10.Measure(new SKSize(((SKRect)(ref bounds)).Width, ((SKRect)(ref bounds)).Height)); - } - else - { - float length = basis.Length; - item = (flag ? item10.Measure(new SKSize(length, ((SKRect)(ref bounds)).Height)) : item10.Measure(new SKSize(((SKRect)(ref bounds)).Width, length))); - } - list2.Add((item10, item, grow, shrink)); - num3 += (flag ? ((SKSize)(ref item)).Width : ((SKSize)(ref item)).Height); - num4 += grow; - num5 += shrink; - } - float num6 = num - num3; - List<(SkiaView, float, float)> list3 = new List<(SkiaView, float, float)>(); - foreach (var item11 in list2) - { - SkiaView item2 = item11.Item1; - SKSize item3 = item11.Item2; - float item4 = item11.Item3; - float item5 = item11.Item4; - float num7 = (flag ? ((SKSize)(ref item3)).Width : ((SKSize)(ref item3)).Height); - float item6 = (flag ? ((SKSize)(ref item3)).Height : ((SKSize)(ref item3)).Width); - if (num6 > 0f && num4 > 0f) - { - num7 += num6 * (item4 / num4); - } - else if (num6 < 0f && num5 > 0f) - { - num7 += num6 * (item5 / num5); - } - list3.Add((item2, Math.Max(0f, num7), item6)); - } - float num8 = list3.Sum<(SkiaView, float, float)>(((SkiaView child, float mainSize, float crossSize) s) => s.mainSize); - float num9 = Math.Max(0f, num - num8); - float num10 = (flag ? ((SKRect)(ref bounds)).Left : ((SKRect)(ref bounds)).Top); - float num11 = 0f; - switch (JustifyContent) - { - case FlexJustify.Center: - num10 += num9 / 2f; - break; - case FlexJustify.End: - num10 += num9; - break; - case FlexJustify.SpaceBetween: - if (list3.Count > 1) - { - num11 = num9 / (float)(list3.Count - 1); - } - break; - case FlexJustify.SpaceAround: - if (list3.Count > 0) - { - num11 = num9 / (float)list3.Count; - num10 += num11 / 2f; - } - break; - case FlexJustify.SpaceEvenly: - if (list3.Count > 0) - { - num11 = num9 / (float)(list3.Count + 1); - num10 += num11; - } - break; - } - float num12 = num10; - IEnumerable<(SkiaView, float, float)> enumerable2; - if (!flag2) - { - IEnumerable<(SkiaView, float, float)> enumerable = list3; - enumerable2 = enumerable; - } - else - { - enumerable2 = list3.AsEnumerable().Reverse(); - } - SKRect bounds2 = default(SKRect); - foreach (var item12 in enumerable2) - { - SkiaView item7 = item12.Item1; - float item8 = item12.Item2; - float item9 = item12.Item3; - FlexAlignSelf alignSelf = GetAlignSelf(item7); - FlexAlignItems flexAlignItems = ((alignSelf == FlexAlignSelf.Auto) ? AlignItems : ((FlexAlignItems)alignSelf)); - float num13 = (flag ? ((SKRect)(ref bounds)).Top : ((SKRect)(ref bounds)).Left); - float num14 = item9; - switch (flexAlignItems) - { - case FlexAlignItems.End: - num13 = (flag ? ((SKRect)(ref bounds)).Bottom : ((SKRect)(ref bounds)).Right) - num14; - break; - case FlexAlignItems.Center: - num13 += (num2 - num14) / 2f; - break; - case FlexAlignItems.Stretch: - num14 = num2; - break; - } - if (flag) - { - ((SKRect)(ref bounds2))._002Ector(num12, num13, num12 + item8, num13 + num14); - } - else - { - ((SKRect)(ref bounds2))._002Ector(num13, num12, num13 + num14, num12 + item8); - } - item7.Arrange(bounds2); - num12 += item8 + num11; - } - return bounds; - } -} diff --git a/Views/SkiaFlyoutPage.cs b/Views/SkiaFlyoutPage.cs index 37c02b6..cb32247 100644 --- a/Views/SkiaFlyoutPage.cs +++ b/Views/SkiaFlyoutPage.cs @@ -1,360 +1,381 @@ -using System; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using SkiaSharp; namespace Microsoft.Maui.Platform; +/// +/// A page that displays a flyout menu and detail content. +/// public class SkiaFlyoutPage : SkiaLayoutView { - private SkiaView? _flyout; + private SkiaView? _flyout; + private SkiaView? _detail; + private bool _isPresented = false; + private float _flyoutWidth = 300f; + private float _flyoutAnimationProgress = 0f; + private bool _gestureEnabled = true; - private SkiaView? _detail; + // Gesture tracking + private bool _isDragging = false; + private float _dragStartX; + private float _dragCurrentX; - private bool _isPresented; + /// + /// Gets or sets the flyout content (menu). + /// + public SkiaView? Flyout + { + get => _flyout; + set + { + if (_flyout != value) + { + if (_flyout != null) + { + RemoveChild(_flyout); + } - private float _flyoutWidth = 300f; + _flyout = value; - private float _flyoutAnimationProgress; + if (_flyout != null) + { + AddChild(_flyout); + } - private bool _gestureEnabled = true; + Invalidate(); + } + } + } - private bool _isDragging; + /// + /// Gets or sets the detail content (main content). + /// + public SkiaView? Detail + { + get => _detail; + set + { + if (_detail != value) + { + if (_detail != null) + { + RemoveChild(_detail); + } - private float _dragStartX; + _detail = value; - private float _dragCurrentX; + if (_detail != null) + { + AddChild(_detail); + } - public SkiaView? Flyout - { - get - { - return _flyout; - } - set - { - if (_flyout != value) - { - if (_flyout != null) - { - RemoveChild(_flyout); - } - _flyout = value; - if (_flyout != null) - { - AddChild(_flyout); - } - Invalidate(); - } - } - } + Invalidate(); + } + } + } - public SkiaView? Detail - { - get - { - return _detail; - } - set - { - if (_detail != value) - { - if (_detail != null) - { - RemoveChild(_detail); - } - _detail = value; - if (_detail != null) - { - AddChild(_detail); - } - Invalidate(); - } - } - } + /// + /// Gets or sets whether the flyout is currently presented. + /// + public bool IsPresented + { + get => _isPresented; + set + { + if (_isPresented != value) + { + _isPresented = value; + _flyoutAnimationProgress = value ? 1f : 0f; + IsPresentedChanged?.Invoke(this, EventArgs.Empty); + Invalidate(); + } + } + } - public bool IsPresented - { - get - { - return _isPresented; - } - set - { - if (_isPresented != value) - { - _isPresented = value; - _flyoutAnimationProgress = (value ? 1f : 0f); - this.IsPresentedChanged?.Invoke(this, EventArgs.Empty); - Invalidate(); - } - } - } + /// + /// Gets or sets the width of the flyout panel. + /// + public float FlyoutWidth + { + get => _flyoutWidth; + set + { + if (_flyoutWidth != value) + { + _flyoutWidth = Math.Max(100, value); + InvalidateMeasure(); + Invalidate(); + } + } + } - public float FlyoutWidth - { - get - { - return _flyoutWidth; - } - set - { - if (_flyoutWidth != value) - { - _flyoutWidth = Math.Max(100f, value); - InvalidateMeasure(); - Invalidate(); - } - } - } + /// + /// Gets or sets whether swipe gestures are enabled. + /// + public bool GestureEnabled + { + get => _gestureEnabled; + set => _gestureEnabled = value; + } - public bool GestureEnabled - { - get - { - return _gestureEnabled; - } - set - { - _gestureEnabled = value; - } - } + /// + /// The flyout layout behavior. + /// + public FlyoutLayoutBehavior FlyoutLayoutBehavior { get; set; } = FlyoutLayoutBehavior.Default; - public FlyoutLayoutBehavior FlyoutLayoutBehavior { get; set; } + /// + /// Background color of the scrim when flyout is open. + /// + public SKColor ScrimColor { get; set; } = new SKColor(0, 0, 0, 100); - public SKColor ScrimColor { get; set; } = new SKColor((byte)0, (byte)0, (byte)0, (byte)100); + /// + /// Shadow width for the flyout. + /// + public float ShadowWidth { get; set; } = 8f; - public float ShadowWidth { get; set; } = 8f; + /// + /// Event raised when IsPresented changes. + /// + public event EventHandler? IsPresentedChanged; - public event EventHandler? IsPresentedChanged; + protected override SKSize MeasureOverride(SKSize availableSize) + { + // Measure flyout + if (_flyout != null) + { + _flyout.Measure(new SKSize(FlyoutWidth, availableSize.Height)); + } - protected override SKSize MeasureOverride(SKSize availableSize) - { - //IL_001b: Unknown result type (might be due to invalid IL or missing references) - //IL_0020: Unknown result type (might be due to invalid IL or missing references) - //IL_003b: Unknown result type (might be due to invalid IL or missing references) - //IL_0034: Unknown result type (might be due to invalid IL or missing references) - //IL_0035: Unknown result type (might be due to invalid IL or missing references) - if (_flyout != null) - { - _flyout.Measure(new SKSize(FlyoutWidth, ((SKSize)(ref availableSize)).Height)); - } - if (_detail != null) - { - _detail.Measure(availableSize); - } - return availableSize; - } + // Measure detail to full size + if (_detail != null) + { + _detail.Measure(availableSize); + } - protected override SKRect ArrangeOverride(SKRect bounds) - { - //IL_000e: Unknown result type (might be due to invalid IL or missing references) - //IL_0063: Unknown result type (might be due to invalid IL or missing references) - //IL_005d: Unknown result type (might be due to invalid IL or missing references) - if (_detail != null) - { - _detail.Arrange(bounds); - } - if (_flyout != null) - { - float num = ((SKRect)(ref bounds)).Left - FlyoutWidth + FlyoutWidth * _flyoutAnimationProgress; - SKRect bounds2 = default(SKRect); - ((SKRect)(ref bounds2))._002Ector(num, ((SKRect)(ref bounds)).Top, num + FlyoutWidth, ((SKRect)(ref bounds)).Bottom); - _flyout.Arrange(bounds2); - } - return bounds; - } + return availableSize; + } - protected override void OnDraw(SKCanvas canvas, SKRect bounds) - { - //IL_0008: Unknown result type (might be due to invalid IL or missing references) - //IL_002f: Unknown result type (might be due to invalid IL or missing references) - //IL_0034: Unknown result type (might be due to invalid IL or missing references) - //IL_0036: Unknown result type (might be due to invalid IL or missing references) - //IL_003b: Unknown result type (might be due to invalid IL or missing references) - //IL_003f: Unknown result type (might be due to invalid IL or missing references) - //IL_0044: Unknown result type (might be due to invalid IL or missing references) - //IL_0055: Unknown result type (might be due to invalid IL or missing references) - //IL_005f: Unknown result type (might be due to invalid IL or missing references) - //IL_0067: Expected O, but got Unknown - //IL_0069: Unknown result type (might be due to invalid IL or missing references) - canvas.Save(); - canvas.ClipRect(bounds, (SKClipOperation)1, false); - _detail?.Draw(canvas); - if (_flyoutAnimationProgress > 0f) - { - SKPaint val = new SKPaint(); - SKColor scrimColor = ScrimColor; - SKColor scrimColor2 = ScrimColor; - val.Color = ((SKColor)(ref scrimColor)).WithAlpha((byte)((float)(int)((SKColor)(ref scrimColor2)).Alpha * _flyoutAnimationProgress)); - val.Style = (SKPaintStyle)0; - SKPaint val2 = val; - try - { - canvas.DrawRect(base.Bounds, val2); - if (_flyout != null && ShadowWidth > 0f) - { - DrawFlyoutShadow(canvas); - } - _flyout?.Draw(canvas); - } - finally - { - ((IDisposable)val2)?.Dispose(); - } - } - canvas.Restore(); - } + protected override SKRect ArrangeOverride(SKRect bounds) + { + // Arrange detail to fill the entire area + if (_detail != null) + { + _detail.Arrange(bounds); + } - private void DrawFlyoutShadow(SKCanvas canvas) - { - //IL_000f: Unknown result type (might be due to invalid IL or missing references) - //IL_0014: Unknown result type (might be due to invalid IL or missing references) - //IL_0021: Unknown result type (might be due to invalid IL or missing references) - //IL_0026: Unknown result type (might be due to invalid IL or missing references) - //IL_0037: Unknown result type (might be due to invalid IL or missing references) - //IL_003c: Unknown result type (might be due to invalid IL or missing references) - //IL_0049: Unknown result type (might be due to invalid IL or missing references) - //IL_0050: Expected O, but got Unknown - //IL_0060: Unknown result type (might be due to invalid IL or missing references) - //IL_0073: Unknown result type (might be due to invalid IL or missing references) - //IL_0085: Unknown result type (might be due to invalid IL or missing references) - //IL_008a: Unknown result type (might be due to invalid IL or missing references) - //IL_0091: Unknown result type (might be due to invalid IL or missing references) - //IL_0096: Unknown result type (might be due to invalid IL or missing references) - //IL_00ab: Unknown result type (might be due to invalid IL or missing references) - if (_flyout == null) - { - return; - } - SKRect bounds = _flyout.Bounds; - float right = ((SKRect)(ref bounds)).Right; - bounds = base.Bounds; - float top = ((SKRect)(ref bounds)).Top; - float num = right + ShadowWidth; - bounds = base.Bounds; - SKRect val = default(SKRect); - ((SKRect)(ref val))._002Ector(right, top, num, ((SKRect)(ref bounds)).Bottom); - SKPaint val2 = new SKPaint(); - val2.Shader = SKShader.CreateLinearGradient(new SKPoint(((SKRect)(ref val)).Left, ((SKRect)(ref val)).MidY), new SKPoint(((SKRect)(ref val)).Right, ((SKRect)(ref val)).MidY), (SKColor[])(object)new SKColor[2] - { - new SKColor((byte)0, (byte)0, (byte)0, (byte)60), - SKColors.Transparent - }, (float[])null, (SKShaderTileMode)0); - SKPaint val3 = val2; - try - { - canvas.DrawRect(val, val3); - } - finally - { - ((IDisposable)val3)?.Dispose(); - } - } + // Arrange flyout (positioned based on animation progress) + if (_flyout != null) + { + float flyoutX = bounds.Left - FlyoutWidth + (FlyoutWidth * _flyoutAnimationProgress); + var flyoutBounds = new SKRect( + flyoutX, + bounds.Top, + flyoutX + FlyoutWidth, + bounds.Bottom); + _flyout.Arrange(flyoutBounds); + } - public override SkiaView? HitTest(float x, float y) - { - //IL_0009: Unknown result type (might be due to invalid IL or missing references) - //IL_000e: Unknown result type (might be due to invalid IL or missing references) - if (base.IsVisible) - { - SKRect bounds = base.Bounds; - if (((SKRect)(ref bounds)).Contains(x, y)) - { - if (_flyoutAnimationProgress > 0f && _flyout != null) - { - SkiaView skiaView = _flyout.HitTest(x, y); - if (skiaView != null) - { - return skiaView; - } - if (_isPresented) - { - return this; - } - } - if (_detail != null) - { - SkiaView skiaView2 = _detail.HitTest(x, y); - if (skiaView2 != null) - { - return skiaView2; - } - } - return this; - } - } - return null; - } + return bounds; + } - public override void OnPointerPressed(PointerEventArgs e) - { - //IL_001f: Unknown result type (might be due to invalid IL or missing references) - //IL_0024: Unknown result type (might be due to invalid IL or missing references) - if (!base.IsEnabled) - { - return; - } - if (_isPresented && _flyout != null) - { - SKRect bounds = _flyout.Bounds; - if (!((SKRect)(ref bounds)).Contains(e.X, e.Y)) - { - IsPresented = false; - e.Handled = true; - return; - } - } - if (_gestureEnabled) - { - _isDragging = true; - _dragStartX = e.X; - _dragCurrentX = e.X; - } - base.OnPointerPressed(e); - } + protected override void OnDraw(SKCanvas canvas, SKRect bounds) + { + canvas.Save(); + canvas.ClipRect(bounds); - public override void OnPointerMoved(PointerEventArgs e) - { - if (_isDragging && _gestureEnabled) - { - _dragCurrentX = e.X; - float num = _dragCurrentX - _dragStartX; - if (_isPresented) - { - _flyoutAnimationProgress = Math.Clamp(1f + num / FlyoutWidth, 0f, 1f); - } - else if (_dragStartX < 30f) - { - _flyoutAnimationProgress = Math.Clamp(num / FlyoutWidth, 0f, 1f); - } - Invalidate(); - e.Handled = true; - } - base.OnPointerMoved(e); - } + // Draw detail content first + _detail?.Draw(canvas); - public override void OnPointerReleased(PointerEventArgs e) - { - if (_isDragging) - { - _isDragging = false; - if (_flyoutAnimationProgress > 0.5f) - { - _isPresented = true; - _flyoutAnimationProgress = 1f; - } - else - { - _isPresented = false; - _flyoutAnimationProgress = 0f; - } - this.IsPresentedChanged?.Invoke(this, EventArgs.Empty); - Invalidate(); - } - base.OnPointerReleased(e); - } + // If flyout is visible, draw scrim and flyout + if (_flyoutAnimationProgress > 0) + { + // Draw scrim (semi-transparent overlay) + using var scrimPaint = new SKPaint + { + Color = ScrimColor.WithAlpha((byte)(ScrimColor.Alpha * _flyoutAnimationProgress)), + Style = SKPaintStyle.Fill + }; + canvas.DrawRect(Bounds, scrimPaint); - public void ToggleFlyout() - { - IsPresented = !IsPresented; - } + // Draw flyout shadow + if (_flyout != null && ShadowWidth > 0) + { + DrawFlyoutShadow(canvas); + } + + // Draw flyout + _flyout?.Draw(canvas); + } + + canvas.Restore(); + } + + private void DrawFlyoutShadow(SKCanvas canvas) + { + if (_flyout == null) return; + + float shadowRight = _flyout.Bounds.Right; + var shadowRect = new SKRect( + shadowRight, + Bounds.Top, + shadowRight + ShadowWidth, + Bounds.Bottom); + + using var shadowPaint = new SKPaint + { + Shader = SKShader.CreateLinearGradient( + new SKPoint(shadowRect.Left, shadowRect.MidY), + new SKPoint(shadowRect.Right, shadowRect.MidY), + new SKColor[] { new SKColor(0, 0, 0, 60), SKColors.Transparent }, + null, + SKShaderTileMode.Clamp) + }; + + canvas.DrawRect(shadowRect, shadowPaint); + } + + public override SkiaView? HitTest(float x, float y) + { + if (!IsVisible || !Bounds.Contains(x, y)) return null; + + // If flyout is presented, check if hit is in flyout + if (_flyoutAnimationProgress > 0 && _flyout != null) + { + var flyoutHit = _flyout.HitTest(x, y); + if (flyoutHit != null) return flyoutHit; + + // Hit on scrim closes flyout + if (_isPresented) + { + return this; // Return self to handle scrim tap + } + } + + // Check detail content + if (_detail != null) + { + var detailHit = _detail.HitTest(x, y); + if (detailHit != null) return detailHit; + } + + return this; + } + + public override void OnPointerPressed(PointerEventArgs e) + { + if (!IsEnabled) return; + + // Check if tap is on scrim (outside flyout but flyout is open) + if (_isPresented && _flyout != null && !_flyout.Bounds.Contains(e.X, e.Y)) + { + IsPresented = false; + e.Handled = true; + return; + } + + // Start drag gesture + if (_gestureEnabled) + { + _isDragging = true; + _dragStartX = e.X; + _dragCurrentX = e.X; + } + + base.OnPointerPressed(e); + } + + public override void OnPointerMoved(PointerEventArgs e) + { + if (_isDragging && _gestureEnabled) + { + _dragCurrentX = e.X; + float delta = _dragCurrentX - _dragStartX; + + // Calculate new animation progress + if (_isPresented) + { + // Dragging to close + _flyoutAnimationProgress = Math.Clamp(1f + (delta / FlyoutWidth), 0f, 1f); + } + else + { + // Dragging to open (only from left edge) + if (_dragStartX < 30) + { + _flyoutAnimationProgress = Math.Clamp(delta / FlyoutWidth, 0f, 1f); + } + } + + Invalidate(); + e.Handled = true; + } + + base.OnPointerMoved(e); + } + + public override void OnPointerReleased(PointerEventArgs e) + { + if (_isDragging) + { + _isDragging = false; + + // Determine final state based on progress + if (_flyoutAnimationProgress > 0.5f) + { + _isPresented = true; + _flyoutAnimationProgress = 1f; + } + else + { + _isPresented = false; + _flyoutAnimationProgress = 0f; + } + + IsPresentedChanged?.Invoke(this, EventArgs.Empty); + Invalidate(); + } + + base.OnPointerReleased(e); + } + + /// + /// Toggles the flyout presentation state. + /// + public void ToggleFlyout() + { + IsPresented = !IsPresented; + } +} + +/// +/// Defines how the flyout behaves. +/// +public enum FlyoutLayoutBehavior +{ + /// + /// Default behavior based on device/window size. + /// + Default, + + /// + /// Flyout slides over the detail content. + /// + Popover, + + /// + /// Flyout and detail are shown side by side. + /// + Split, + + /// + /// Flyout pushes the detail content. + /// + SplitOnLandscape, + + /// + /// Flyout is always shown in portrait, side by side in landscape. + /// + SplitOnPortrait } diff --git a/Views/SkiaFrame.cs b/Views/SkiaFrame.cs deleted file mode 100644 index e5026b2..0000000 --- a/Views/SkiaFrame.cs +++ /dev/null @@ -1,18 +0,0 @@ -using SkiaSharp; - -namespace Microsoft.Maui.Platform; - -public class SkiaFrame : SkiaBorder -{ - public SkiaFrame() - { - //IL_0024: Unknown result type (might be due to invalid IL or missing references) - //IL_002f: Unknown result type (might be due to invalid IL or missing references) - base.HasShadow = true; - base.CornerRadius = 4f; - SetPadding(10f); - base.BackgroundColor = SKColors.White; - base.Stroke = SKColors.Transparent; - base.StrokeThickness = 0f; - } -} diff --git a/Views/SkiaGraphicsView.cs b/Views/SkiaGraphicsView.cs index 2db8034..8c192fe 100644 --- a/Views/SkiaGraphicsView.cs +++ b/Views/SkiaGraphicsView.cs @@ -1,81 +1,65 @@ -using System; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using SkiaSharp; using Microsoft.Maui.Graphics; using Microsoft.Maui.Graphics.Skia; -using SkiaSharp; namespace Microsoft.Maui.Platform; +/// +/// Skia-rendered graphics view that supports IDrawable for custom drawing. +/// public class SkiaGraphicsView : SkiaView { - private IDrawable? _drawable; + private IDrawable? _drawable; - public IDrawable? Drawable - { - get - { - return _drawable; - } - set - { - _drawable = value; - Invalidate(); - } - } + public IDrawable? Drawable + { + get => _drawable; + set + { + _drawable = value; + Invalidate(); + } + } - protected override void OnDraw(SKCanvas canvas, SKRect bounds) - { - //IL_0001: Unknown result type (might be due to invalid IL or missing references) - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - //IL_0012: Unknown result type (might be due to invalid IL or missing references) - //IL_0017: Unknown result type (might be due to invalid IL or missing references) - //IL_0019: Unknown result type (might be due to invalid IL or missing references) - //IL_0023: Unknown result type (might be due to invalid IL or missing references) - //IL_002b: Expected O, but got Unknown - //IL_006a: Unknown result type (might be due to invalid IL or missing references) - //IL_0070: Expected O, but got Unknown - //IL_002c: Unknown result type (might be due to invalid IL or missing references) - //IL_007e: Unknown result type (might be due to invalid IL or missing references) - if (base.BackgroundColor != SKColors.Transparent) - { - SKPaint val = new SKPaint - { - Color = base.BackgroundColor, - Style = (SKPaintStyle)0 - }; - try - { - canvas.DrawRect(bounds, val); - } - finally - { - ((IDisposable)val)?.Dispose(); - } - } - if (_drawable != null) - { - RectF val2 = default(RectF); - ((RectF)(ref val2))._002Ector(((SKRect)(ref bounds)).Left, ((SKRect)(ref bounds)).Top, ((SKRect)(ref bounds)).Width, ((SKRect)(ref bounds)).Height); - SkiaCanvas val3 = new SkiaCanvas(); - try - { - val3.Canvas = canvas; - _drawable.Draw((ICanvas)(object)val3, val2); - } - finally - { - ((IDisposable)val3)?.Dispose(); - } - } - } + protected override void OnDraw(SKCanvas canvas, SKRect bounds) + { + // Draw background + if (BackgroundColor != SKColors.Transparent) + { + using var bgPaint = new SKPaint + { + Color = BackgroundColor, + Style = SKPaintStyle.Fill + }; + canvas.DrawRect(bounds, bgPaint); + } - protected override SKSize MeasureOverride(SKSize availableSize) - { - //IL_001c: Unknown result type (might be due to invalid IL or missing references) - //IL_0056: Unknown result type (might be due to invalid IL or missing references) - if (((SKSize)(ref availableSize)).Width < float.MaxValue && ((SKSize)(ref availableSize)).Height < float.MaxValue) - { - return availableSize; - } - return new SKSize((((SKSize)(ref availableSize)).Width < float.MaxValue) ? ((SKSize)(ref availableSize)).Width : 100f, (((SKSize)(ref availableSize)).Height < float.MaxValue) ? ((SKSize)(ref availableSize)).Height : 100f); - } + // Draw using IDrawable + if (_drawable != null) + { + var dirtyRect = new RectF(bounds.Left, bounds.Top, bounds.Width, bounds.Height); + + using var skiaCanvas = new SkiaCanvas(); + skiaCanvas.Canvas = canvas; + + _drawable.Draw(skiaCanvas, dirtyRect); + } + } + + protected override SKSize MeasureOverride(SKSize availableSize) + { + // Graphics view takes all available space by default + if (availableSize.Width < float.MaxValue && availableSize.Height < float.MaxValue) + { + return availableSize; + } + + // Return a reasonable default size + return new SKSize( + availableSize.Width < float.MaxValue ? availableSize.Width : 100, + availableSize.Height < float.MaxValue ? availableSize.Height : 100); + } } diff --git a/Views/SkiaGrid.cs b/Views/SkiaGrid.cs deleted file mode 100644 index 289f200..0000000 --- a/Views/SkiaGrid.cs +++ /dev/null @@ -1,406 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.Maui.Controls; -using SkiaSharp; - -namespace Microsoft.Maui.Platform; - -public class SkiaGrid : SkiaLayoutView -{ - public static readonly BindableProperty RowSpacingProperty = BindableProperty.Create("RowSpacing", typeof(float), typeof(SkiaGrid), (object)0f, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaGrid)(object)b).InvalidateMeasure(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty ColumnSpacingProperty = BindableProperty.Create("ColumnSpacing", typeof(float), typeof(SkiaGrid), (object)0f, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaGrid)(object)b).InvalidateMeasure(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - private readonly List _rowDefinitions = new List(); - - private readonly List _columnDefinitions = new List(); - - private readonly Dictionary _childPositions = new Dictionary(); - - private float[] _rowHeights = Array.Empty(); - - private float[] _columnWidths = Array.Empty(); - - public IList RowDefinitions => _rowDefinitions; - - public IList ColumnDefinitions => _columnDefinitions; - - public float RowSpacing - { - get - { - return (float)((BindableObject)this).GetValue(RowSpacingProperty); - } - set - { - ((BindableObject)this).SetValue(RowSpacingProperty, (object)value); - } - } - - public float ColumnSpacing - { - get - { - return (float)((BindableObject)this).GetValue(ColumnSpacingProperty); - } - set - { - ((BindableObject)this).SetValue(ColumnSpacingProperty, (object)value); - } - } - - public void AddChild(SkiaView child, int row, int column, int rowSpan = 1, int columnSpan = 1) - { - base.AddChild(child); - _childPositions[child] = new GridPosition(row, column, rowSpan, columnSpan); - } - - public override void RemoveChild(SkiaView child) - { - base.RemoveChild(child); - _childPositions.Remove(child); - } - - public GridPosition GetPosition(SkiaView child) - { - if (!_childPositions.TryGetValue(child, out var value)) - { - return new GridPosition(0, 0); - } - return value; - } - - public void SetPosition(SkiaView child, int row, int column, int rowSpan = 1, int columnSpan = 1) - { - _childPositions[child] = new GridPosition(row, column, rowSpan, columnSpan); - InvalidateMeasure(); - Invalidate(); - } - - protected override SKSize MeasureOverride(SKSize availableSize) - { - //IL_0008: Unknown result type (might be due to invalid IL or missing references) - //IL_000d: Unknown result type (might be due to invalid IL or missing references) - //IL_0018: Unknown result type (might be due to invalid IL or missing references) - //IL_001d: Unknown result type (might be due to invalid IL or missing references) - //IL_0030: Unknown result type (might be due to invalid IL or missing references) - //IL_0035: Unknown result type (might be due to invalid IL or missing references) - //IL_0040: Unknown result type (might be due to invalid IL or missing references) - //IL_0045: Unknown result type (might be due to invalid IL or missing references) - //IL_015f: Unknown result type (might be due to invalid IL or missing references) - //IL_0164: Unknown result type (might be due to invalid IL or missing references) - //IL_0169: Unknown result type (might be due to invalid IL or missing references) - //IL_022a: Unknown result type (might be due to invalid IL or missing references) - //IL_022f: Unknown result type (might be due to invalid IL or missing references) - //IL_0234: Unknown result type (might be due to invalid IL or missing references) - //IL_0333: Unknown result type (might be due to invalid IL or missing references) - //IL_0338: Unknown result type (might be due to invalid IL or missing references) - //IL_0392: Unknown result type (might be due to invalid IL or missing references) - //IL_0397: Unknown result type (might be due to invalid IL or missing references) - //IL_03a2: Unknown result type (might be due to invalid IL or missing references) - //IL_03a7: Unknown result type (might be due to invalid IL or missing references) - //IL_03b4: Unknown result type (might be due to invalid IL or missing references) - //IL_03b9: Unknown result type (might be due to invalid IL or missing references) - //IL_03c4: Unknown result type (might be due to invalid IL or missing references) - //IL_03c9: Unknown result type (might be due to invalid IL or missing references) - //IL_03d3: Unknown result type (might be due to invalid IL or missing references) - float width = ((SKSize)(ref availableSize)).Width; - SKRect padding = base.Padding; - float num = width - ((SKRect)(ref padding)).Left; - padding = base.Padding; - float num2 = num - ((SKRect)(ref padding)).Right; - float height = ((SKSize)(ref availableSize)).Height; - padding = base.Padding; - float num3 = height - ((SKRect)(ref padding)).Top; - padding = base.Padding; - float num4 = num3 - ((SKRect)(ref padding)).Bottom; - if (float.IsNaN(num2) || float.IsInfinity(num2)) - { - num2 = 800f; - } - if (float.IsNaN(num4) || float.IsInfinity(num4)) - { - num4 = float.PositiveInfinity; - } - int num5 = Math.Max(1, (_rowDefinitions.Count > 0) ? _rowDefinitions.Count : (GetMaxRow() + 1)); - int num6 = Math.Max(1, (_columnDefinitions.Count > 0) ? _columnDefinitions.Count : (GetMaxColumn() + 1)); - float[] array = new float[num6]; - float[] array2 = new float[num5]; - foreach (SkiaView child in base.Children) - { - if (child.IsVisible) - { - GridPosition position = GetPosition(child); - if (((position.Column < _columnDefinitions.Count) ? _columnDefinitions[position.Column] : GridLength.Star).IsAuto && position.ColumnSpan == 1) - { - SKSize val = child.Measure(new SKSize(float.PositiveInfinity, float.PositiveInfinity)); - float val2 = (float.IsNaN(((SKSize)(ref val)).Width) ? 0f : ((SKSize)(ref val)).Width); - array[position.Column] = Math.Max(array[position.Column], val2); - } - } - } - _columnWidths = CalculateSizesWithAuto(_columnDefinitions, num2, ColumnSpacing, num6, array); - foreach (SkiaView child2 in base.Children) - { - if (child2.IsVisible) - { - GridPosition position2 = GetPosition(child2); - float cellWidth = GetCellWidth(position2.Column, position2.ColumnSpan); - SKSize val3 = child2.Measure(new SKSize(cellWidth, float.PositiveInfinity)); - float num7 = ((SKSize)(ref val3)).Height; - if (float.IsNaN(num7) || float.IsInfinity(num7) || num7 > 100000f) - { - num7 = 44f; - } - if (position2.RowSpan == 1) - { - array2[position2.Row] = Math.Max(array2[position2.Row], num7); - } - } - } - if (float.IsInfinity(num4) || num4 > 100000f) - { - _rowHeights = array2; - } - else - { - _rowHeights = CalculateSizesWithAuto(_rowDefinitions, num4, RowSpacing, num5, array2); - } - foreach (SkiaView child3 in base.Children) - { - if (child3.IsVisible) - { - GridPosition position3 = GetPosition(child3); - float cellWidth2 = GetCellWidth(position3.Column, position3.ColumnSpan); - float cellHeight = GetCellHeight(position3.Row, position3.RowSpan); - child3.Measure(new SKSize(cellWidth2, cellHeight)); - } - } - float num8 = _columnWidths.Sum() + (float)Math.Max(0, num6 - 1) * ColumnSpacing; - float num9 = _rowHeights.Sum() + (float)Math.Max(0, num5 - 1) * RowSpacing; - padding = base.Padding; - float num10 = num8 + ((SKRect)(ref padding)).Left; - padding = base.Padding; - float num11 = num10 + ((SKRect)(ref padding)).Right; - padding = base.Padding; - float num12 = num9 + ((SKRect)(ref padding)).Top; - padding = base.Padding; - return new SKSize(num11, num12 + ((SKRect)(ref padding)).Bottom); - } - - private int GetMaxRow() - { - int num = 0; - foreach (GridPosition value in _childPositions.Values) - { - num = Math.Max(num, value.Row + value.RowSpan - 1); - } - return num; - } - - private int GetMaxColumn() - { - int num = 0; - foreach (GridPosition value in _childPositions.Values) - { - num = Math.Max(num, value.Column + value.ColumnSpan - 1); - } - return num; - } - - private float[] CalculateSizesWithAuto(List definitions, float available, float spacing, int count, float[] naturalSizes) - { - if (count == 0) - { - return new float[1] { available }; - } - float[] array = new float[count]; - float num = (float)Math.Max(0, count - 1) * spacing; - float num2 = available - num; - float num3 = 0f; - for (int i = 0; i < count; i++) - { - GridLength gridLength = ((i < definitions.Count) ? definitions[i] : GridLength.Star); - if (gridLength.IsAbsolute) - { - array[i] = gridLength.Value; - num2 -= gridLength.Value; - } - else if (gridLength.IsAuto) - { - array[i] = naturalSizes[i]; - num2 -= array[i]; - } - else if (gridLength.IsStar) - { - num3 += gridLength.Value; - } - } - if (num3 > 0f && num2 > 0f) - { - for (int j = 0; j < count; j++) - { - GridLength gridLength2 = ((j < definitions.Count) ? definitions[j] : GridLength.Star); - if (gridLength2.IsStar) - { - array[j] = gridLength2.Value / num3 * num2; - } - } - } - return array; - } - - private float GetCellWidth(int column, int span) - { - float num = 0f; - for (int i = column; i < Math.Min(column + span, _columnWidths.Length); i++) - { - num += _columnWidths[i]; - if (i > column) - { - num += ColumnSpacing; - } - } - return num; - } - - private float GetCellHeight(int row, int span) - { - float num = 0f; - for (int i = row; i < Math.Min(row + span, _rowHeights.Length); i++) - { - num += _rowHeights[i]; - if (i > row) - { - num += RowSpacing; - } - } - return num; - } - - private float GetColumnOffset(int column) - { - float num = 0f; - for (int i = 0; i < Math.Min(column, _columnWidths.Length); i++) - { - num += _columnWidths[i] + ColumnSpacing; - } - return num; - } - - private float GetRowOffset(int row) - { - float num = 0f; - for (int i = 0; i < Math.Min(row, _rowHeights.Length); i++) - { - num += _rowHeights[i] + RowSpacing; - } - return num; - } - - protected override SKRect ArrangeOverride(SKRect bounds) - { - //IL_0001: Unknown result type (might be due to invalid IL or missing references) - //IL_0002: Unknown result type (might be due to invalid IL or missing references) - //IL_0007: Unknown result type (might be due to invalid IL or missing references) - //IL_0349: Unknown result type (might be due to invalid IL or missing references) - //IL_0300: Unknown result type (might be due to invalid IL or missing references) - //IL_0301: Unknown result type (might be due to invalid IL or missing references) - //IL_03ca: Unknown result type (might be due to invalid IL or missing references) - //IL_029d: Unknown result type (might be due to invalid IL or missing references) - //IL_02a2: Unknown result type (might be due to invalid IL or missing references) - //IL_02df: Unknown result type (might be due to invalid IL or missing references) - try - { - SKRect contentBounds = GetContentBounds(bounds); - int num = ((_rowHeights.Length == 0) ? 1 : _rowHeights.Length); - if (_columnWidths.Length != 0) - { - _ = _columnWidths.Length; - } - float[] array = _rowHeights; - if (((SKRect)(ref contentBounds)).Height > 0f && !float.IsInfinity(((SKRect)(ref contentBounds)).Height)) - { - float num2 = _rowHeights.Sum() + (float)Math.Max(0, num - 1) * RowSpacing; - if (((SKRect)(ref contentBounds)).Height > num2 + 1f) - { - array = new float[num]; - float num3 = ((SKRect)(ref contentBounds)).Height - num2; - float num4 = 0f; - for (int i = 0; i < num; i++) - { - GridLength gridLength = ((i < _rowDefinitions.Count) ? _rowDefinitions[i] : GridLength.Star); - if (gridLength.IsStar) - { - num4 += gridLength.Value; - } - } - for (int j = 0; j < num; j++) - { - GridLength gridLength2 = ((j < _rowDefinitions.Count) ? _rowDefinitions[j] : GridLength.Star); - array[j] = ((j < _rowHeights.Length) ? _rowHeights[j] : 0f); - if (gridLength2.IsStar && num4 > 0f) - { - array[j] += num3 * (gridLength2.Value / num4); - } - } - } - else - { - array = _rowHeights; - } - } - SKRect bounds2 = default(SKRect); - foreach (SkiaView child in base.Children) - { - if (!child.IsVisible) - { - continue; - } - GridPosition position = GetPosition(child); - float num5 = ((SKRect)(ref contentBounds)).Left + GetColumnOffset(position.Column); - float num6 = ((SKRect)(ref contentBounds)).Top; - for (int k = 0; k < Math.Min(position.Row, array.Length); k++) - { - num6 += array[k] + RowSpacing; - } - float num7 = GetCellWidth(position.Column, position.ColumnSpan); - float num8 = 0f; - for (int l = position.Row; l < Math.Min(position.Row + position.RowSpan, array.Length); l++) - { - num8 += array[l]; - if (l > position.Row) - { - num8 += RowSpacing; - } - } - if (float.IsInfinity(num7) || float.IsNaN(num7)) - { - num7 = ((SKRect)(ref contentBounds)).Width; - } - if (float.IsInfinity(num8) || float.IsNaN(num8) || num8 <= 0f) - { - num8 = ((SKRect)(ref contentBounds)).Height; - } - Thickness margin = child.Margin; - ((SKRect)(ref bounds2))._002Ector(num5 + (float)((Thickness)(ref margin)).Left, num6 + (float)((Thickness)(ref margin)).Top, num5 + num7 - (float)((Thickness)(ref margin)).Right, num6 + num8 - (float)((Thickness)(ref margin)).Bottom); - child.Arrange(bounds2); - } - return bounds; - } - catch (Exception ex) - { - Console.WriteLine("[SkiaGrid] EXCEPTION in ArrangeOverride: " + ex.GetType().Name + ": " + ex.Message); - Console.WriteLine($"[SkiaGrid] Bounds: {bounds}, RowHeights: {_rowHeights.Length}, RowDefs: {_rowDefinitions.Count}, Children: {base.Children.Count}"); - Console.WriteLine("[SkiaGrid] Stack trace: " + ex.StackTrace); - throw; - } - } -} diff --git a/Views/SkiaImage.cs b/Views/SkiaImage.cs index 42f4384..8cef851 100644 --- a/Views/SkiaImage.cs +++ b/Views/SkiaImage.cs @@ -1,553 +1,263 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using SkiaSharp; -using Svg.Skia; +using Microsoft.Maui.Graphics; namespace Microsoft.Maui.Platform; +/// +/// Skia-rendered image control. +/// public class SkiaImage : SkiaView { - private SKBitmap? _bitmap; + private SKBitmap? _bitmap; + private SKImage? _image; + private bool _isLoading; - private SKImage? _image; + public SKBitmap? Bitmap + { + get => _bitmap; + set + { + _bitmap?.Dispose(); + _bitmap = value; + _image?.Dispose(); + _image = value != null ? SKImage.FromBitmap(value) : null; + Invalidate(); + } + } - private bool _isLoading; + public Aspect Aspect { get; set; } = Aspect.AspectFit; + public bool IsOpaque { get; set; } + public bool IsLoading => _isLoading; + public bool IsAnimationPlaying { get; set; } - private string? _currentFilePath; + public event EventHandler? ImageLoaded; + public event EventHandler? ImageLoadingError; - private bool _isSvg; + protected override void OnDraw(SKCanvas canvas, SKRect bounds) + { + // Draw background if not opaque + if (!IsOpaque && BackgroundColor != SKColors.Transparent) + { + using var bgPaint = new SKPaint + { + Color = BackgroundColor, + Style = SKPaintStyle.Fill + }; + canvas.DrawRect(bounds, bgPaint); + } - private CancellationTokenSource? _loadCts; + if (_image == null) return; - private readonly object _loadLock = new object(); + var imageWidth = _image.Width; + var imageHeight = _image.Height; - private double _svgLoadedWidth; + if (imageWidth <= 0 || imageHeight <= 0) return; - private double _svgLoadedHeight; + var destRect = CalculateDestRect(bounds, imageWidth, imageHeight); - private bool _pendingSvgReload; + using var paint = new SKPaint + { + IsAntialias = true, + FilterQuality = SKFilterQuality.High + }; - private SKRect _lastArrangedBounds; + canvas.DrawImage(_image, destRect, paint); + } - public SKBitmap? Bitmap - { - get - { - return _bitmap; - } - set - { - SKBitmap? bitmap = _bitmap; - if (bitmap != null) - { - ((SKNativeObject)bitmap).Dispose(); - } - _bitmap = value; - SKImage? image = _image; - if (image != null) - { - ((SKNativeObject)image).Dispose(); - } - _image = ((value != null) ? SKImage.FromBitmap(value) : null); - Invalidate(); - } - } + private SKRect CalculateDestRect(SKRect bounds, float imageWidth, float imageHeight) + { + float destX, destY, destWidth, destHeight; - public Aspect Aspect { get; set; } + switch (Aspect) + { + case Aspect.Fill: + // Stretch to fill entire bounds + return bounds; - public bool IsOpaque { get; set; } + case Aspect.AspectFit: + // Scale to fit while maintaining aspect ratio + var fitScale = Math.Min(bounds.Width / imageWidth, bounds.Height / imageHeight); + destWidth = imageWidth * fitScale; + destHeight = imageHeight * fitScale; + destX = bounds.Left + (bounds.Width - destWidth) / 2; + destY = bounds.Top + (bounds.Height - destHeight) / 2; + return new SKRect(destX, destY, destX + destWidth, destY + destHeight); - public bool IsLoading => _isLoading; + case Aspect.AspectFill: + // Scale to fill while maintaining aspect ratio (may crop) + var fillScale = Math.Max(bounds.Width / imageWidth, bounds.Height / imageHeight); + destWidth = imageWidth * fillScale; + destHeight = imageHeight * fillScale; + destX = bounds.Left + (bounds.Width - destWidth) / 2; + destY = bounds.Top + (bounds.Height - destHeight) / 2; + return new SKRect(destX, destY, destX + destWidth, destY + destHeight); - public bool IsAnimationPlaying { get; set; } + case Aspect.Center: + // Center without scaling + destX = bounds.Left + (bounds.Width - imageWidth) / 2; + destY = bounds.Top + (bounds.Height - imageHeight) / 2; + return new SKRect(destX, destY, destX + imageWidth, destY + imageHeight); - public new double WidthRequest - { - get - { - return base.WidthRequest; - } - set - { - base.WidthRequest = value; - ScheduleSvgReloadIfNeeded(); - } - } + default: + return bounds; + } + } - public new double HeightRequest - { - get - { - return base.HeightRequest; - } - set - { - base.HeightRequest = value; - ScheduleSvgReloadIfNeeded(); - } - } + public async Task LoadFromFileAsync(string filePath) + { + _isLoading = true; + Invalidate(); - public event EventHandler? ImageLoaded; + try + { + await Task.Run(() => + { + using var stream = File.OpenRead(filePath); + var bitmap = SKBitmap.Decode(stream); + if (bitmap != null) + { + Bitmap = bitmap; + } + }); - public event EventHandler? ImageLoadingError; + _isLoading = false; + ImageLoaded?.Invoke(this, EventArgs.Empty); + } + catch (Exception ex) + { + _isLoading = false; + ImageLoadingError?.Invoke(this, new ImageLoadingErrorEventArgs(ex)); + } - private void ScheduleSvgReloadIfNeeded() - { - if (_isSvg && !string.IsNullOrEmpty(_currentFilePath)) - { - double widthRequest = WidthRequest; - double heightRequest = HeightRequest; - if (widthRequest > 0.0 && heightRequest > 0.0 && (Math.Abs(_svgLoadedWidth - widthRequest) > 0.5 || Math.Abs(_svgLoadedHeight - heightRequest) > 0.5) && !_pendingSvgReload) - { - _pendingSvgReload = true; - ReloadSvgDebounced(); - } - } - } + Invalidate(); + } - private async Task ReloadSvgDebounced() - { - await Task.Delay(10); - _pendingSvgReload = false; - if (!string.IsNullOrEmpty(_currentFilePath) && WidthRequest > 0.0 && HeightRequest > 0.0) - { - Console.WriteLine($"[SkiaImage] Reloading SVG at {WidthRequest}x{HeightRequest} (was {_svgLoadedWidth}x{_svgLoadedHeight})"); - await LoadSvgAtSizeAsync(_currentFilePath, WidthRequest, HeightRequest); - } - } + public async Task LoadFromStreamAsync(Stream stream) + { + _isLoading = true; + Invalidate(); - protected override void OnDraw(SKCanvas canvas, SKRect bounds) - { - //IL_0009: Unknown result type (might be due to invalid IL or missing references) - //IL_000e: Unknown result type (might be due to invalid IL or missing references) - //IL_001a: Unknown result type (might be due to invalid IL or missing references) - //IL_001f: Unknown result type (might be due to invalid IL or missing references) - //IL_0021: Unknown result type (might be due to invalid IL or missing references) - //IL_002b: Unknown result type (might be due to invalid IL or missing references) - //IL_0034: Expected O, but got Unknown - //IL_0035: Unknown result type (might be due to invalid IL or missing references) - //IL_0076: Unknown result type (might be due to invalid IL or missing references) - //IL_007b: Unknown result type (might be due to invalid IL or missing references) - //IL_0080: Unknown result type (might be due to invalid IL or missing references) - //IL_0081: Unknown result type (might be due to invalid IL or missing references) - //IL_0086: Unknown result type (might be due to invalid IL or missing references) - //IL_008d: Unknown result type (might be due to invalid IL or missing references) - //IL_0095: Expected O, but got Unknown - //IL_009c: Unknown result type (might be due to invalid IL or missing references) - if (!IsOpaque && base.BackgroundColor != SKColors.Transparent) - { - SKPaint val = new SKPaint - { - Color = base.BackgroundColor, - Style = (SKPaintStyle)0 - }; - try - { - canvas.DrawRect(bounds, val); - } - finally - { - ((IDisposable)val)?.Dispose(); - } - } - if (_image == null) - { - return; - } - int width = _image.Width; - int height = _image.Height; - if (width <= 0 || height <= 0) - { - return; - } - SKRect val2 = CalculateDestRect(bounds, width, height); - SKPaint val3 = new SKPaint - { - IsAntialias = true, - FilterQuality = (SKFilterQuality)3 - }; - try - { - canvas.DrawImage(_image, val2, val3); - } - finally - { - ((IDisposable)val3)?.Dispose(); - } - } + try + { + await Task.Run(() => + { + var bitmap = SKBitmap.Decode(stream); + if (bitmap != null) + { + Bitmap = bitmap; + } + }); - private SKRect CalculateDestRect(SKRect bounds, float imageWidth, float imageHeight) - { - //IL_0001: Unknown result type (might be due to invalid IL or missing references) - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - //IL_0008: Unknown result type (might be due to invalid IL or missing references) - //IL_001f: Expected I4, but got Unknown - //IL_0081: Unknown result type (might be due to invalid IL or missing references) - //IL_00e2: Unknown result type (might be due to invalid IL or missing references) - //IL_0024: Unknown result type (might be due to invalid IL or missing references) - //IL_0120: Unknown result type (might be due to invalid IL or missing references) - //IL_0126: Unknown result type (might be due to invalid IL or missing references) - Aspect aspect = Aspect; - switch ((int)aspect) - { - case 2: - return bounds; - case 0: - { - float num6 = Math.Min(((SKRect)(ref bounds)).Width / imageWidth, ((SKRect)(ref bounds)).Height / imageHeight); - float num4 = imageWidth * num6; - float num5 = imageHeight * num6; - float num = ((SKRect)(ref bounds)).Left + (((SKRect)(ref bounds)).Width - num4) / 2f; - float num2 = ((SKRect)(ref bounds)).Top + (((SKRect)(ref bounds)).Height - num5) / 2f; - return new SKRect(num, num2, num + num4, num2 + num5); - } - case 1: - { - float num3 = Math.Max(((SKRect)(ref bounds)).Width / imageWidth, ((SKRect)(ref bounds)).Height / imageHeight); - float num4 = imageWidth * num3; - float num5 = imageHeight * num3; - float num = ((SKRect)(ref bounds)).Left + (((SKRect)(ref bounds)).Width - num4) / 2f; - float num2 = ((SKRect)(ref bounds)).Top + (((SKRect)(ref bounds)).Height - num5) / 2f; - return new SKRect(num, num2, num + num4, num2 + num5); - } - case 3: - { - float num = ((SKRect)(ref bounds)).Left + (((SKRect)(ref bounds)).Width - imageWidth) / 2f; - float num2 = ((SKRect)(ref bounds)).Top + (((SKRect)(ref bounds)).Height - imageHeight) / 2f; - return new SKRect(num, num2, num + imageWidth, num2 + imageHeight); - } - default: - return bounds; - } - } + _isLoading = false; + ImageLoaded?.Invoke(this, EventArgs.Empty); + } + catch (Exception ex) + { + _isLoading = false; + ImageLoadingError?.Invoke(this, new ImageLoadingErrorEventArgs(ex)); + } - public async Task LoadFromFileAsync(string filePath) - { - _isLoading = true; - Invalidate(); - Console.WriteLine($"[SkiaImage] LoadFromFileAsync: {filePath}, WidthRequest={WidthRequest}, HeightRequest={HeightRequest}"); - try - { - List list = new List - { - filePath, - Path.Combine(AppContext.BaseDirectory, filePath), - Path.Combine(AppContext.BaseDirectory, "Resources", "Images", filePath), - Path.Combine(AppContext.BaseDirectory, "Resources", filePath) - }; - if (filePath.EndsWith(".png", StringComparison.OrdinalIgnoreCase)) - { - string text = Path.ChangeExtension(filePath, ".svg"); - list.Add(text); - list.Add(Path.Combine(AppContext.BaseDirectory, text)); - list.Add(Path.Combine(AppContext.BaseDirectory, "Resources", "Images", text)); - list.Add(Path.Combine(AppContext.BaseDirectory, "Resources", text)); - } - string foundPath = null; - foreach (string item in list) - { - if (File.Exists(item)) - { - foundPath = item; - Console.WriteLine("[SkiaImage] Found file at: " + item); - break; - } - } - if (foundPath == null) - { - Console.WriteLine("[SkiaImage] File not found: " + filePath); - _isLoading = false; - _isSvg = false; - _currentFilePath = null; - this.ImageLoadingError?.Invoke(this, new ImageLoadingErrorEventArgs(new FileNotFoundException(filePath))); - return; - } - _isSvg = foundPath.EndsWith(".svg", StringComparison.OrdinalIgnoreCase); - _currentFilePath = foundPath; - if (!_isSvg) - { - await Task.Run(delegate - { - using FileStream fileStream = File.OpenRead(foundPath); - SKBitmap val = SKBitmap.Decode((Stream)fileStream); - if (val != null) - { - Bitmap = val; - Console.WriteLine("[SkiaImage] Loaded image: " + foundPath); - } - }); - } - else - { - await LoadSvgAtSizeAsync(foundPath, WidthRequest, HeightRequest); - } - _isLoading = false; - this.ImageLoaded?.Invoke(this, EventArgs.Empty); - } - catch (Exception exception) - { - _isLoading = false; - this.ImageLoadingError?.Invoke(this, new ImageLoadingErrorEventArgs(exception)); - } - Invalidate(); - } + Invalidate(); + } - private async Task LoadSvgAtSizeAsync(string svgPath, double targetWidth, double targetHeight) - { - _loadCts?.Cancel(); - CancellationTokenSource cts = new CancellationTokenSource(); - _loadCts = cts; - try - { - SKBitmap newBitmap = null; - await Task.Run(delegate - { - //IL_0016: Unknown result type (might be due to invalid IL or missing references) - //IL_001c: Expected O, but got Unknown - //IL_0052: Unknown result type (might be due to invalid IL or missing references) - //IL_0057: Unknown result type (might be due to invalid IL or missing references) - //IL_0109: Unknown result type (might be due to invalid IL or missing references) - //IL_0113: Expected O, but got Unknown - //IL_0119: Unknown result type (might be due to invalid IL or missing references) - //IL_0120: Expected O, but got Unknown - //IL_0122: Unknown result type (might be due to invalid IL or missing references) - if (cts.Token.IsCancellationRequested) - { - return; - } - SKSvg val = new SKSvg(); - try - { - val.Load(svgPath); - if (val.Picture != null && !cts.Token.IsCancellationRequested) - { - SKRect cullRect = val.Picture.CullRect; - float num = ((targetWidth > 0.0) ? ((float)targetWidth) : ((((SKRect)(ref cullRect)).Width <= 24f) ? 24f : ((SKRect)(ref cullRect)).Width)); - float num2 = Math.Min(val2: ((targetHeight > 0.0) ? ((float)targetHeight) : ((((SKRect)(ref cullRect)).Height <= 24f) ? 24f : ((SKRect)(ref cullRect)).Height)) / ((SKRect)(ref cullRect)).Height, val1: num / ((SKRect)(ref cullRect)).Width); - int num3 = Math.Max(1, (int)(((SKRect)(ref cullRect)).Width * num2)); - int num4 = Math.Max(1, (int)(((SKRect)(ref cullRect)).Height * num2)); - newBitmap = new SKBitmap(num3, num4, false); - SKCanvas val2 = new SKCanvas(newBitmap); - try - { - val2.Clear(SKColors.Transparent); - val2.Scale(num2); - val2.DrawPicture(val.Picture, (SKPaint)null); - Console.WriteLine($"[SkiaImage] Loaded SVG: {svgPath} at {num3}x{num4} (requested {targetWidth}x{targetHeight})"); - return; - } - finally - { - ((IDisposable)val2)?.Dispose(); - } - } - } - finally - { - ((IDisposable)val)?.Dispose(); - } - }, cts.Token); - if (!cts.Token.IsCancellationRequested && newBitmap != null) - { - _svgLoadedWidth = ((targetWidth > 0.0) ? targetWidth : ((double)newBitmap.Width)); - _svgLoadedHeight = ((targetHeight > 0.0) ? targetHeight : ((double)newBitmap.Height)); - Bitmap = newBitmap; - return; - } - SKBitmap obj = newBitmap; - if (obj != null) - { - ((SKNativeObject)obj).Dispose(); - } - } - catch (OperationCanceledException) - { - } - } + public async Task LoadFromUriAsync(Uri uri) + { + _isLoading = true; + Invalidate(); - public async Task LoadFromStreamAsync(Stream stream) - { - _isLoading = true; - Invalidate(); - try - { - await Task.Run(delegate - { - SKBitmap val = SKBitmap.Decode(stream); - if (val != null) - { - Bitmap = val; - } - }); - _isLoading = false; - this.ImageLoaded?.Invoke(this, EventArgs.Empty); - } - catch (Exception exception) - { - _isLoading = false; - this.ImageLoadingError?.Invoke(this, new ImageLoadingErrorEventArgs(exception)); - } - Invalidate(); - } + try + { + using var httpClient = new HttpClient(); + var data = await httpClient.GetByteArrayAsync(uri); - public async Task LoadFromUriAsync(Uri uri) - { - _isLoading = true; - Invalidate(); - try - { - using HttpClient httpClient = new HttpClient(); - using MemoryStream memoryStream = new MemoryStream(await httpClient.GetByteArrayAsync(uri)); - SKBitmap val = SKBitmap.Decode((Stream)memoryStream); - if (val != null) - { - Bitmap = val; - } - _isLoading = false; - this.ImageLoaded?.Invoke(this, EventArgs.Empty); - } - catch (Exception exception) - { - _isLoading = false; - this.ImageLoadingError?.Invoke(this, new ImageLoadingErrorEventArgs(exception)); - } - Invalidate(); - } + using var stream = new MemoryStream(data); + var bitmap = SKBitmap.Decode(stream); + if (bitmap != null) + { + Bitmap = bitmap; + } - public void LoadFromData(byte[] data) - { - try - { - using MemoryStream memoryStream = new MemoryStream(data); - SKBitmap val = SKBitmap.Decode((Stream)memoryStream); - if (val != null) - { - Bitmap = val; - } - this.ImageLoaded?.Invoke(this, EventArgs.Empty); - } - catch (Exception exception) - { - this.ImageLoadingError?.Invoke(this, new ImageLoadingErrorEventArgs(exception)); - } - } + _isLoading = false; + ImageLoaded?.Invoke(this, EventArgs.Empty); + } + catch (Exception ex) + { + _isLoading = false; + ImageLoadingError?.Invoke(this, new ImageLoadingErrorEventArgs(ex)); + } - public void LoadFromBitmap(SKBitmap bitmap) - { - try - { - _isSvg = false; - _currentFilePath = null; - Bitmap = bitmap; - _isLoading = false; - this.ImageLoaded?.Invoke(this, EventArgs.Empty); - } - catch (Exception exception) - { - _isLoading = false; - this.ImageLoadingError?.Invoke(this, new ImageLoadingErrorEventArgs(exception)); - } - Invalidate(); - } + Invalidate(); + } - public override void Arrange(SKRect bounds) - { - //IL_0001: Unknown result type (might be due to invalid IL or missing references) - //IL_00c1: Unknown result type (might be due to invalid IL or missing references) - //IL_00c2: Unknown result type (might be due to invalid IL or missing references) - base.Arrange(bounds); - if ((!(base.WidthRequest > 0.0) || !(base.HeightRequest > 0.0)) && _isSvg && !string.IsNullOrEmpty(_currentFilePath) && !_isLoading) - { - float width = ((SKRect)(ref bounds)).Width; - float height = ((SKRect)(ref bounds)).Height; - if (((double)width > _svgLoadedWidth * 1.1 || (double)height > _svgLoadedHeight * 1.1) && width > 0f && height > 0f && (width != ((SKRect)(ref _lastArrangedBounds)).Width || height != ((SKRect)(ref _lastArrangedBounds)).Height)) - { - _lastArrangedBounds = bounds; - Console.WriteLine($"[SkiaImage] Arrange detected larger bounds: {width}x{height} vs loaded {_svgLoadedWidth}x{_svgLoadedHeight}"); - LoadSvgAtSizeAsync(_currentFilePath, width, height); - } - } - } + public void LoadFromData(byte[] data) + { + try + { + using var stream = new MemoryStream(data); + var bitmap = SKBitmap.Decode(stream); + if (bitmap != null) + { + Bitmap = bitmap; + } + ImageLoaded?.Invoke(this, EventArgs.Empty); + } + catch (Exception ex) + { + ImageLoadingError?.Invoke(this, new ImageLoadingErrorEventArgs(ex)); + } + } - protected override SKSize MeasureOverride(SKSize availableSize) - { - //IL_002a: Unknown result type (might be due to invalid IL or missing references) - //IL_00a6: Unknown result type (might be due to invalid IL or missing references) - //IL_0048: Unknown result type (might be due to invalid IL or missing references) - //IL_00c4: Unknown result type (might be due to invalid IL or missing references) - //IL_006e: Unknown result type (might be due to invalid IL or missing references) - //IL_005e: Unknown result type (might be due to invalid IL or missing references) - //IL_0131: Unknown result type (might be due to invalid IL or missing references) - //IL_0107: Unknown result type (might be due to invalid IL or missing references) - //IL_0163: Unknown result type (might be due to invalid IL or missing references) - //IL_015b: Unknown result type (might be due to invalid IL or missing references) - double widthRequest = base.WidthRequest; - double heightRequest = base.HeightRequest; - if (widthRequest > 0.0 && heightRequest > 0.0) - { - return new SKSize((float)widthRequest, (float)heightRequest); - } - if (_image == null) - { - if (widthRequest > 0.0) - { - return new SKSize((float)widthRequest, (float)widthRequest); - } - if (heightRequest > 0.0) - { - return new SKSize((float)heightRequest, (float)heightRequest); - } - return new SKSize(100f, 100f); - } - float num = _image.Width; - float num2 = _image.Height; - if (widthRequest > 0.0) - { - float num3 = (float)widthRequest / num; - return new SKSize((float)widthRequest, num2 * num3); - } - if (heightRequest > 0.0) - { - float num4 = (float)heightRequest / num2; - return new SKSize(num * num4, (float)heightRequest); - } - if (((SKSize)(ref availableSize)).Width < float.MaxValue && ((SKSize)(ref availableSize)).Height < float.MaxValue) - { - float num5 = Math.Min(((SKSize)(ref availableSize)).Width / num, ((SKSize)(ref availableSize)).Height / num2); - return new SKSize(num * num5, num2 * num5); - } - if (((SKSize)(ref availableSize)).Width < float.MaxValue) - { - float num6 = ((SKSize)(ref availableSize)).Width / num; - return new SKSize(((SKSize)(ref availableSize)).Width, num2 * num6); - } - if (((SKSize)(ref availableSize)).Height < float.MaxValue) - { - float num7 = ((SKSize)(ref availableSize)).Height / num2; - return new SKSize(num * num7, ((SKSize)(ref availableSize)).Height); - } - return new SKSize(num, num2); - } + protected override SKSize MeasureOverride(SKSize availableSize) + { + if (_image == null) + return new SKSize(100, 100); // Default size - protected override void Dispose(bool disposing) - { - if (disposing) - { - SKBitmap? bitmap = _bitmap; - if (bitmap != null) - { - ((SKNativeObject)bitmap).Dispose(); - } - SKImage? image = _image; - if (image != null) - { - ((SKNativeObject)image).Dispose(); - } - } - base.Dispose(disposing); - } + var imageWidth = _image.Width; + var imageHeight = _image.Height; + + // If we have constraints, respect them + if (availableSize.Width < float.MaxValue && availableSize.Height < float.MaxValue) + { + var scale = Math.Min(availableSize.Width / imageWidth, availableSize.Height / imageHeight); + return new SKSize(imageWidth * scale, imageHeight * scale); + } + else if (availableSize.Width < float.MaxValue) + { + var scale = availableSize.Width / imageWidth; + return new SKSize(availableSize.Width, imageHeight * scale); + } + else if (availableSize.Height < float.MaxValue) + { + var scale = availableSize.Height / imageHeight; + return new SKSize(imageWidth * scale, availableSize.Height); + } + + return new SKSize(imageWidth, imageHeight); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _bitmap?.Dispose(); + _image?.Dispose(); + } + base.Dispose(disposing); + } +} + +/// +/// Event args for image loading errors. +/// +public class ImageLoadingErrorEventArgs : EventArgs +{ + public Exception Exception { get; } + + public ImageLoadingErrorEventArgs(Exception exception) + { + Exception = exception; + } } diff --git a/Views/SkiaImageButton.cs b/Views/SkiaImageButton.cs index 3e2dbc2..30740d7 100644 --- a/Views/SkiaImageButton.cs +++ b/Views/SkiaImageButton.cs @@ -1,612 +1,438 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Net.Http; -using System.Threading.Tasks; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using SkiaSharp; -using Svg.Skia; +using Microsoft.Maui.Graphics; namespace Microsoft.Maui.Platform; +/// +/// Skia-rendered image button control. +/// Combines button behavior with image display. +/// public class SkiaImageButton : SkiaView { - private SKBitmap? _bitmap; + private SKBitmap? _bitmap; + private SKImage? _image; + private bool _isLoading; - private SKImage? _image; + public SKBitmap? Bitmap + { + get => _bitmap; + set + { + _bitmap?.Dispose(); + _bitmap = value; + _image?.Dispose(); + _image = value != null ? SKImage.FromBitmap(value) : null; + Invalidate(); + } + } - private bool _isLoading; + // Image properties + public Aspect Aspect { get; set; } = Aspect.AspectFit; + public bool IsOpaque { get; set; } + public bool IsLoading => _isLoading; - public SKBitmap? Bitmap - { - get - { - return _bitmap; - } - set - { - SKBitmap? bitmap = _bitmap; - if (bitmap != null) - { - ((SKNativeObject)bitmap).Dispose(); - } - _bitmap = value; - SKImage? image = _image; - if (image != null) - { - ((SKNativeObject)image).Dispose(); - } - _image = ((value != null) ? SKImage.FromBitmap(value) : null); - Invalidate(); - } - } + // Button stroke properties + public SKColor StrokeColor { get; set; } = SKColors.Transparent; + public float StrokeThickness { get; set; } = 0; + public float CornerRadius { get; set; } = 0; - public Aspect Aspect { get; set; } + // Button state + public bool IsPressed { get; private set; } + public bool IsHovered { get; private set; } - public bool IsOpaque { get; set; } + // Visual state colors + public SKColor PressedBackgroundColor { get; set; } = new SKColor(0, 0, 0, 30); + public SKColor HoveredBackgroundColor { get; set; } = new SKColor(0, 0, 0, 15); - public bool IsLoading => _isLoading; + // Padding for the image content + public float PaddingLeft { get; set; } + public float PaddingTop { get; set; } + public float PaddingRight { get; set; } + public float PaddingBottom { get; set; } - public SKColor StrokeColor { get; set; } = SKColors.Transparent; + public event EventHandler? Clicked; + public event EventHandler? Pressed; + public event EventHandler? Released; + public event EventHandler? ImageLoaded; + public event EventHandler? ImageLoadingError; - public float StrokeThickness { get; set; } + public SkiaImageButton() + { + IsFocusable = true; + } - public float CornerRadius { get; set; } + protected override void OnDraw(SKCanvas canvas, SKRect bounds) + { + // Apply padding + var contentBounds = new SKRect( + bounds.Left + PaddingLeft, + bounds.Top + PaddingTop, + bounds.Right - PaddingRight, + bounds.Bottom - PaddingBottom); - public bool IsPressed { get; private set; } + // Draw background based on state + if (IsPressed || IsHovered || !IsOpaque && BackgroundColor != SKColors.Transparent) + { + var bgColor = IsPressed ? PressedBackgroundColor + : IsHovered ? HoveredBackgroundColor + : BackgroundColor; - public bool IsHovered { get; private set; } + using var bgPaint = new SKPaint + { + Color = bgColor, + Style = SKPaintStyle.Fill, + IsAntialias = true + }; - public SKColor PressedBackgroundColor { get; set; } = new SKColor((byte)0, (byte)0, (byte)0, (byte)30); + if (CornerRadius > 0) + { + var roundRect = new SKRoundRect(bounds, CornerRadius); + canvas.DrawRoundRect(roundRect, bgPaint); + } + else + { + canvas.DrawRect(bounds, bgPaint); + } + } - public SKColor HoveredBackgroundColor { get; set; } = new SKColor((byte)0, (byte)0, (byte)0, (byte)15); + // Draw image + if (_image != null) + { + var imageWidth = _image.Width; + var imageHeight = _image.Height; - public float PaddingLeft { get; set; } + if (imageWidth > 0 && imageHeight > 0) + { + var destRect = CalculateDestRect(contentBounds, imageWidth, imageHeight); - public float PaddingTop { get; set; } + using var paint = new SKPaint + { + IsAntialias = true, + FilterQuality = SKFilterQuality.High + }; - public float PaddingRight { get; set; } + // Apply opacity when disabled + if (!IsEnabled) + { + paint.Color = paint.Color.WithAlpha(128); + } - public float PaddingBottom { get; set; } + canvas.DrawImage(_image, destRect, paint); + } + } - public event EventHandler? Clicked; + // Draw stroke/border + if (StrokeThickness > 0 && StrokeColor != SKColors.Transparent) + { + using var strokePaint = new SKPaint + { + Color = StrokeColor, + Style = SKPaintStyle.Stroke, + StrokeWidth = StrokeThickness, + IsAntialias = true + }; - public event EventHandler? Pressed; + if (CornerRadius > 0) + { + var roundRect = new SKRoundRect(bounds, CornerRadius); + canvas.DrawRoundRect(roundRect, strokePaint); + } + else + { + canvas.DrawRect(bounds, strokePaint); + } + } - public event EventHandler? Released; + // Draw focus ring + if (IsFocused) + { + using var focusPaint = new SKPaint + { + Color = new SKColor(0x00, 0x00, 0x00, 0x40), + Style = SKPaintStyle.Stroke, + StrokeWidth = 2, + IsAntialias = true + }; - public event EventHandler? ImageLoaded; + var focusBounds = new SKRect(bounds.Left - 2, bounds.Top - 2, bounds.Right + 2, bounds.Bottom + 2); + if (CornerRadius > 0) + { + var focusRect = new SKRoundRect(focusBounds, CornerRadius + 2); + canvas.DrawRoundRect(focusRect, focusPaint); + } + else + { + canvas.DrawRect(focusBounds, focusPaint); + } + } + } - public event EventHandler? ImageLoadingError; + private SKRect CalculateDestRect(SKRect bounds, float imageWidth, float imageHeight) + { + float destX, destY, destWidth, destHeight; - public SkiaImageButton() - { - //IL_0001: Unknown result type (might be due to invalid IL or missing references) - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - //IL_0011: Unknown result type (might be due to invalid IL or missing references) - //IL_0016: Unknown result type (might be due to invalid IL or missing references) - //IL_0021: Unknown result type (might be due to invalid IL or missing references) - //IL_0026: Unknown result type (might be due to invalid IL or missing references) - base.IsFocusable = true; - } + switch (Aspect) + { + case Aspect.Fill: + return bounds; - protected override void OnDraw(SKCanvas canvas, SKRect bounds) - { - //IL_008d: Unknown result type (might be due to invalid IL or missing references) - //IL_0092: Unknown result type (might be due to invalid IL or missing references) - //IL_0093: Unknown result type (might be due to invalid IL or missing references) - //IL_0098: Unknown result type (might be due to invalid IL or missing references) - //IL_0099: Unknown result type (might be due to invalid IL or missing references) - //IL_009f: Unknown result type (might be due to invalid IL or missing references) - //IL_00a6: Unknown result type (might be due to invalid IL or missing references) - //IL_00ae: Expected O, but got Unknown - //IL_0085: Unknown result type (might be due to invalid IL or missing references) - //IL_007d: Unknown result type (might be due to invalid IL or missing references) - //IL_005b: Unknown result type (might be due to invalid IL or missing references) - //IL_0060: Unknown result type (might be due to invalid IL or missing references) - //IL_00d3: Unknown result type (might be due to invalid IL or missing references) - //IL_00bb: Unknown result type (might be due to invalid IL or missing references) - //IL_00c2: Unknown result type (might be due to invalid IL or missing references) - //IL_00c8: Expected O, but got Unknown - //IL_0189: Unknown result type (might be due to invalid IL or missing references) - //IL_018e: Unknown result type (might be due to invalid IL or missing references) - //IL_0210: Unknown result type (might be due to invalid IL or missing references) - //IL_0215: Unknown result type (might be due to invalid IL or missing references) - //IL_021b: Unknown result type (might be due to invalid IL or missing references) - //IL_0225: Unknown result type (might be due to invalid IL or missing references) - //IL_022c: Unknown result type (might be due to invalid IL or missing references) - //IL_0237: Unknown result type (might be due to invalid IL or missing references) - //IL_0240: Expected O, but got Unknown - //IL_019a: Unknown result type (might be due to invalid IL or missing references) - //IL_019f: Unknown result type (might be due to invalid IL or missing references) - //IL_01a1: Unknown result type (might be due to invalid IL or missing references) - //IL_01ab: Unknown result type (might be due to invalid IL or missing references) - //IL_01b2: Unknown result type (might be due to invalid IL or missing references) - //IL_01be: Unknown result type (might be due to invalid IL or missing references) - //IL_01c7: Expected O, but got Unknown - //IL_0116: Unknown result type (might be due to invalid IL or missing references) - //IL_011d: Unknown result type (might be due to invalid IL or missing references) - //IL_0122: Unknown result type (might be due to invalid IL or missing references) - //IL_0124: Unknown result type (might be due to invalid IL or missing references) - //IL_0129: Unknown result type (might be due to invalid IL or missing references) - //IL_0130: Unknown result type (might be due to invalid IL or missing references) - //IL_0139: Expected O, but got Unknown - //IL_0274: Unknown result type (might be due to invalid IL or missing references) - //IL_0279: Unknown result type (might be due to invalid IL or missing references) - //IL_02aa: Unknown result type (might be due to invalid IL or missing references) - //IL_0288: Unknown result type (might be due to invalid IL or missing references) - //IL_0296: Unknown result type (might be due to invalid IL or missing references) - //IL_029d: Expected O, but got Unknown - //IL_01ef: Unknown result type (might be due to invalid IL or missing references) - //IL_01d4: Unknown result type (might be due to invalid IL or missing references) - //IL_01db: Unknown result type (might be due to invalid IL or missing references) - //IL_01e2: Expected O, but got Unknown - //IL_0164: Unknown result type (might be due to invalid IL or missing references) - //IL_0145: Unknown result type (might be due to invalid IL or missing references) - //IL_014a: Unknown result type (might be due to invalid IL or missing references) - //IL_0153: Unknown result type (might be due to invalid IL or missing references) - SKRect bounds2 = default(SKRect); - ((SKRect)(ref bounds2))._002Ector(((SKRect)(ref bounds)).Left + PaddingLeft, ((SKRect)(ref bounds)).Top + PaddingTop, ((SKRect)(ref bounds)).Right - PaddingRight, ((SKRect)(ref bounds)).Bottom - PaddingBottom); - if (IsPressed || IsHovered || (!IsOpaque && base.BackgroundColor != SKColors.Transparent)) - { - SKColor color = (IsPressed ? PressedBackgroundColor : (IsHovered ? HoveredBackgroundColor : base.BackgroundColor)); - SKPaint val = new SKPaint - { - Color = color, - Style = (SKPaintStyle)0, - IsAntialias = true - }; - try - { - if (CornerRadius > 0f) - { - SKRoundRect val2 = new SKRoundRect(bounds, CornerRadius); - canvas.DrawRoundRect(val2, val); - } - else - { - canvas.DrawRect(bounds, val); - } - } - finally - { - ((IDisposable)val)?.Dispose(); - } - } - if (_image != null) - { - int width = _image.Width; - int height = _image.Height; - if (width > 0 && height > 0) - { - SKRect val3 = CalculateDestRect(bounds2, width, height); - SKPaint val4 = new SKPaint - { - IsAntialias = true, - FilterQuality = (SKFilterQuality)3 - }; - try - { - if (!base.IsEnabled) - { - SKColor color2 = val4.Color; - val4.Color = ((SKColor)(ref color2)).WithAlpha((byte)128); - } - canvas.DrawImage(_image, val3, val4); - } - finally - { - ((IDisposable)val4)?.Dispose(); - } - } - } - if (StrokeThickness > 0f && StrokeColor != SKColors.Transparent) - { - SKPaint val5 = new SKPaint - { - Color = StrokeColor, - Style = (SKPaintStyle)1, - StrokeWidth = StrokeThickness, - IsAntialias = true - }; - try - { - if (CornerRadius > 0f) - { - SKRoundRect val6 = new SKRoundRect(bounds, CornerRadius); - canvas.DrawRoundRect(val6, val5); - } - else - { - canvas.DrawRect(bounds, val5); - } - } - finally - { - ((IDisposable)val5)?.Dispose(); - } - } - if (!base.IsFocused) - { - return; - } - SKPaint val7 = new SKPaint - { - Color = new SKColor((byte)0, (byte)0, (byte)0, (byte)64), - Style = (SKPaintStyle)1, - StrokeWidth = 2f, - IsAntialias = true - }; - try - { - SKRect val8 = new SKRect(((SKRect)(ref bounds)).Left - 2f, ((SKRect)(ref bounds)).Top - 2f, ((SKRect)(ref bounds)).Right + 2f, ((SKRect)(ref bounds)).Bottom + 2f); - if (CornerRadius > 0f) - { - SKRoundRect val9 = new SKRoundRect(val8, CornerRadius + 2f); - canvas.DrawRoundRect(val9, val7); - } - else - { - canvas.DrawRect(val8, val7); - } - } - finally - { - ((IDisposable)val7)?.Dispose(); - } - } + case Aspect.AspectFit: + var fitScale = Math.Min(bounds.Width / imageWidth, bounds.Height / imageHeight); + destWidth = imageWidth * fitScale; + destHeight = imageHeight * fitScale; + destX = bounds.Left + (bounds.Width - destWidth) / 2; + destY = bounds.Top + (bounds.Height - destHeight) / 2; + return new SKRect(destX, destY, destX + destWidth, destY + destHeight); - private SKRect CalculateDestRect(SKRect bounds, float imageWidth, float imageHeight) - { - //IL_0001: Unknown result type (might be due to invalid IL or missing references) - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - //IL_0008: Unknown result type (might be due to invalid IL or missing references) - //IL_001f: Expected I4, but got Unknown - //IL_0081: Unknown result type (might be due to invalid IL or missing references) - //IL_00e2: Unknown result type (might be due to invalid IL or missing references) - //IL_0024: Unknown result type (might be due to invalid IL or missing references) - //IL_0120: Unknown result type (might be due to invalid IL or missing references) - //IL_0126: Unknown result type (might be due to invalid IL or missing references) - Aspect aspect = Aspect; - switch ((int)aspect) - { - case 2: - return bounds; - case 0: - { - float num6 = Math.Min(((SKRect)(ref bounds)).Width / imageWidth, ((SKRect)(ref bounds)).Height / imageHeight); - float num4 = imageWidth * num6; - float num5 = imageHeight * num6; - float num = ((SKRect)(ref bounds)).Left + (((SKRect)(ref bounds)).Width - num4) / 2f; - float num2 = ((SKRect)(ref bounds)).Top + (((SKRect)(ref bounds)).Height - num5) / 2f; - return new SKRect(num, num2, num + num4, num2 + num5); - } - case 1: - { - float num3 = Math.Max(((SKRect)(ref bounds)).Width / imageWidth, ((SKRect)(ref bounds)).Height / imageHeight); - float num4 = imageWidth * num3; - float num5 = imageHeight * num3; - float num = ((SKRect)(ref bounds)).Left + (((SKRect)(ref bounds)).Width - num4) / 2f; - float num2 = ((SKRect)(ref bounds)).Top + (((SKRect)(ref bounds)).Height - num5) / 2f; - return new SKRect(num, num2, num + num4, num2 + num5); - } - case 3: - { - float num = ((SKRect)(ref bounds)).Left + (((SKRect)(ref bounds)).Width - imageWidth) / 2f; - float num2 = ((SKRect)(ref bounds)).Top + (((SKRect)(ref bounds)).Height - imageHeight) / 2f; - return new SKRect(num, num2, num + imageWidth, num2 + imageHeight); - } - default: - return bounds; - } - } + case Aspect.AspectFill: + var fillScale = Math.Max(bounds.Width / imageWidth, bounds.Height / imageHeight); + destWidth = imageWidth * fillScale; + destHeight = imageHeight * fillScale; + destX = bounds.Left + (bounds.Width - destWidth) / 2; + destY = bounds.Top + (bounds.Height - destHeight) / 2; + return new SKRect(destX, destY, destX + destWidth, destY + destHeight); - public async Task LoadFromFileAsync(string filePath) - { - _isLoading = true; - Invalidate(); - Console.WriteLine("[SkiaImageButton] LoadFromFileAsync: " + filePath); - try - { - List list = new List - { - filePath, - Path.Combine(AppContext.BaseDirectory, filePath), - Path.Combine(AppContext.BaseDirectory, "Resources", "Images", filePath), - Path.Combine(AppContext.BaseDirectory, "Resources", filePath) - }; - if (filePath.EndsWith(".png", StringComparison.OrdinalIgnoreCase)) - { - string text = Path.ChangeExtension(filePath, ".svg"); - list.Add(text); - list.Add(Path.Combine(AppContext.BaseDirectory, text)); - list.Add(Path.Combine(AppContext.BaseDirectory, "Resources", "Images", text)); - list.Add(Path.Combine(AppContext.BaseDirectory, "Resources", text)); - } - string foundPath = null; - foreach (string item in list) - { - if (File.Exists(item)) - { - foundPath = item; - Console.WriteLine("[SkiaImageButton] Found file at: " + item); - break; - } - } - if (foundPath == null) - { - Console.WriteLine("[SkiaImageButton] File not found: " + filePath); - Console.WriteLine("[SkiaImageButton] Searched paths: " + string.Join(", ", list)); - _isLoading = false; - this.ImageLoadingError?.Invoke(this, new ImageLoadingErrorEventArgs(new FileNotFoundException(filePath))); - return; - } - await Task.Run(delegate - { - //IL_0016: Unknown result type (might be due to invalid IL or missing references) - //IL_001c: Expected O, but got Unknown - //IL_003a: Unknown result type (might be due to invalid IL or missing references) - //IL_003f: Unknown result type (might be due to invalid IL or missing references) - //IL_0114: Unknown result type (might be due to invalid IL or missing references) - //IL_011b: Expected O, but got Unknown - //IL_011d: Unknown result type (might be due to invalid IL or missing references) - //IL_0124: Expected O, but got Unknown - //IL_0126: Unknown result type (might be due to invalid IL or missing references) - if (foundPath.EndsWith(".svg", StringComparison.OrdinalIgnoreCase)) - { - SKSvg val = new SKSvg(); - try - { - val.Load(foundPath); - if (val.Picture != null) - { - SKRect cullRect = val.Picture.CullRect; - bool num = base.WidthRequest > 0.0; - bool flag = base.HeightRequest > 0.0; - float num2 = (num ? ((float)(base.WidthRequest - (double)PaddingLeft - (double)PaddingRight)) : ((SKRect)(ref cullRect)).Width); - float num3 = Math.Min(val2: (flag ? ((float)(base.HeightRequest - (double)PaddingTop - (double)PaddingBottom)) : ((SKRect)(ref cullRect)).Height) / ((SKRect)(ref cullRect)).Height, val1: num2 / ((SKRect)(ref cullRect)).Width); - int num4 = Math.Max(1, (int)(((SKRect)(ref cullRect)).Width * num3)); - int num5 = Math.Max(1, (int)(((SKRect)(ref cullRect)).Height * num3)); - SKBitmap val2 = new SKBitmap(num4, num5, false); - SKCanvas val3 = new SKCanvas(val2); - try - { - val3.Clear(SKColors.Transparent); - val3.Scale(num3); - val3.DrawPicture(val.Picture, (SKPaint)null); - Bitmap = val2; - Console.WriteLine($"[SkiaImageButton] Loaded SVG: {foundPath} ({num4}x{num5})"); - return; - } - finally - { - ((IDisposable)val3)?.Dispose(); - } - } - return; - } - finally - { - ((IDisposable)val)?.Dispose(); - } - } - using FileStream fileStream = File.OpenRead(foundPath); - SKBitmap val4 = SKBitmap.Decode((Stream)fileStream); - if (val4 != null) - { - Bitmap = val4; - Console.WriteLine("[SkiaImageButton] Loaded image: " + foundPath); - } - }); - _isLoading = false; - this.ImageLoaded?.Invoke(this, EventArgs.Empty); - } - catch (Exception exception) - { - _isLoading = false; - this.ImageLoadingError?.Invoke(this, new ImageLoadingErrorEventArgs(exception)); - } - Invalidate(); - } + case Aspect.Center: + destX = bounds.Left + (bounds.Width - imageWidth) / 2; + destY = bounds.Top + (bounds.Height - imageHeight) / 2; + return new SKRect(destX, destY, destX + imageWidth, destY + imageHeight); - public async Task LoadFromStreamAsync(Stream stream) - { - _isLoading = true; - Invalidate(); - try - { - await Task.Run(delegate - { - SKBitmap val = SKBitmap.Decode(stream); - if (val != null) - { - Bitmap = val; - } - }); - _isLoading = false; - this.ImageLoaded?.Invoke(this, EventArgs.Empty); - } - catch (Exception exception) - { - _isLoading = false; - this.ImageLoadingError?.Invoke(this, new ImageLoadingErrorEventArgs(exception)); - } - Invalidate(); - } + default: + return bounds; + } + } - public async Task LoadFromUriAsync(Uri uri) - { - _isLoading = true; - Invalidate(); - try - { - using HttpClient httpClient = new HttpClient(); - using MemoryStream memoryStream = new MemoryStream(await httpClient.GetByteArrayAsync(uri)); - SKBitmap val = SKBitmap.Decode((Stream)memoryStream); - if (val != null) - { - Bitmap = val; - } - _isLoading = false; - this.ImageLoaded?.Invoke(this, EventArgs.Empty); - } - catch (Exception exception) - { - _isLoading = false; - this.ImageLoadingError?.Invoke(this, new ImageLoadingErrorEventArgs(exception)); - } - Invalidate(); - } + // Image loading methods + public async Task LoadFromFileAsync(string filePath) + { + _isLoading = true; + Invalidate(); - public void LoadFromData(byte[] data) - { - try - { - using MemoryStream memoryStream = new MemoryStream(data); - SKBitmap val = SKBitmap.Decode((Stream)memoryStream); - if (val != null) - { - Bitmap = val; - } - this.ImageLoaded?.Invoke(this, EventArgs.Empty); - } - catch (Exception exception) - { - this.ImageLoadingError?.Invoke(this, new ImageLoadingErrorEventArgs(exception)); - } - } + try + { + await Task.Run(() => + { + using var stream = File.OpenRead(filePath); + var bitmap = SKBitmap.Decode(stream); + if (bitmap != null) + { + Bitmap = bitmap; + } + }); - public override void OnPointerEntered(PointerEventArgs e) - { - if (base.IsEnabled) - { - IsHovered = true; - SkiaVisualStateManager.GoToState(this, "PointerOver"); - Invalidate(); - } - } + _isLoading = false; + ImageLoaded?.Invoke(this, EventArgs.Empty); + } + catch (Exception ex) + { + _isLoading = false; + ImageLoadingError?.Invoke(this, new ImageLoadingErrorEventArgs(ex)); + } - public override void OnPointerExited(PointerEventArgs e) - { - IsHovered = false; - if (IsPressed) - { - IsPressed = false; - } - SkiaVisualStateManager.GoToState(this, base.IsEnabled ? "Normal" : "Disabled"); - Invalidate(); - } + Invalidate(); + } - public override void OnPointerPressed(PointerEventArgs e) - { - if (base.IsEnabled) - { - IsPressed = true; - SkiaVisualStateManager.GoToState(this, "Pressed"); - Invalidate(); - this.Pressed?.Invoke(this, EventArgs.Empty); - } - } + public async Task LoadFromStreamAsync(Stream stream) + { + _isLoading = true; + Invalidate(); - public override void OnPointerReleased(PointerEventArgs e) - { - //IL_0051: Unknown result type (might be due to invalid IL or missing references) - //IL_0056: Unknown result type (might be due to invalid IL or missing references) - //IL_0065: Unknown result type (might be due to invalid IL or missing references) - if (!base.IsEnabled) - { - return; - } - bool isPressed = IsPressed; - IsPressed = false; - SkiaVisualStateManager.GoToState(this, IsHovered ? "PointerOver" : "Normal"); - Invalidate(); - this.Released?.Invoke(this, EventArgs.Empty); - if (isPressed) - { - SKRect bounds = base.Bounds; - if (((SKRect)(ref bounds)).Contains(new SKPoint(e.X, e.Y))) - { - this.Clicked?.Invoke(this, EventArgs.Empty); - } - } - } + try + { + await Task.Run(() => + { + var bitmap = SKBitmap.Decode(stream); + if (bitmap != null) + { + Bitmap = bitmap; + } + }); - public override void OnKeyDown(KeyEventArgs e) - { - if (base.IsEnabled && (e.Key == Key.Enter || e.Key == Key.Space)) - { - IsPressed = true; - Invalidate(); - this.Pressed?.Invoke(this, EventArgs.Empty); - e.Handled = true; - } - } + _isLoading = false; + ImageLoaded?.Invoke(this, EventArgs.Empty); + } + catch (Exception ex) + { + _isLoading = false; + ImageLoadingError?.Invoke(this, new ImageLoadingErrorEventArgs(ex)); + } - public override void OnKeyUp(KeyEventArgs e) - { - if (base.IsEnabled && (e.Key == Key.Enter || e.Key == Key.Space)) - { - if (IsPressed) - { - IsPressed = false; - Invalidate(); - this.Released?.Invoke(this, EventArgs.Empty); - this.Clicked?.Invoke(this, EventArgs.Empty); - } - e.Handled = true; - } - } + Invalidate(); + } - protected override SKSize MeasureOverride(SKSize availableSize) - { - //IL_0043: Unknown result type (might be due to invalid IL or missing references) - //IL_0113: Unknown result type (might be due to invalid IL or missing references) - //IL_00d7: Unknown result type (might be due to invalid IL or missing references) - //IL_0169: Unknown result type (might be due to invalid IL or missing references) - //IL_014f: Unknown result type (might be due to invalid IL or missing references) - SKSize val = default(SKSize); - ((SKSize)(ref val))._002Ector(PaddingLeft + PaddingRight, PaddingTop + PaddingBottom); - if (_image == null) - { - return new SKSize(44f + ((SKSize)(ref val)).Width, 44f + ((SKSize)(ref val)).Height); - } - int width = _image.Width; - int height = _image.Height; - if (((SKSize)(ref availableSize)).Width < float.MaxValue && ((SKSize)(ref availableSize)).Height < float.MaxValue) - { - SKSize val2 = default(SKSize); - ((SKSize)(ref val2))._002Ector(((SKSize)(ref availableSize)).Width - ((SKSize)(ref val)).Width, ((SKSize)(ref availableSize)).Height - ((SKSize)(ref val)).Height); - float num = Math.Min(((SKSize)(ref val2)).Width / (float)width, ((SKSize)(ref val2)).Height / (float)height); - return new SKSize((float)width * num + ((SKSize)(ref val)).Width, (float)height * num + ((SKSize)(ref val)).Height); - } - if (((SKSize)(ref availableSize)).Width < float.MaxValue) - { - float num2 = (((SKSize)(ref availableSize)).Width - ((SKSize)(ref val)).Width) / (float)width; - return new SKSize(((SKSize)(ref availableSize)).Width, (float)height * num2 + ((SKSize)(ref val)).Height); - } - if (((SKSize)(ref availableSize)).Height < float.MaxValue) - { - float num3 = (((SKSize)(ref availableSize)).Height - ((SKSize)(ref val)).Height) / (float)height; - return new SKSize((float)width * num3 + ((SKSize)(ref val)).Width, ((SKSize)(ref availableSize)).Height); - } - return new SKSize((float)width + ((SKSize)(ref val)).Width, (float)height + ((SKSize)(ref val)).Height); - } + public async Task LoadFromUriAsync(Uri uri) + { + _isLoading = true; + Invalidate(); - protected override void Dispose(bool disposing) - { - if (disposing) - { - SKBitmap? bitmap = _bitmap; - if (bitmap != null) - { - ((SKNativeObject)bitmap).Dispose(); - } - SKImage? image = _image; - if (image != null) - { - ((SKNativeObject)image).Dispose(); - } - } - base.Dispose(disposing); - } + try + { + using var httpClient = new HttpClient(); + var data = await httpClient.GetByteArrayAsync(uri); + + using var stream = new MemoryStream(data); + var bitmap = SKBitmap.Decode(stream); + if (bitmap != null) + { + Bitmap = bitmap; + } + + _isLoading = false; + ImageLoaded?.Invoke(this, EventArgs.Empty); + } + catch (Exception ex) + { + _isLoading = false; + ImageLoadingError?.Invoke(this, new ImageLoadingErrorEventArgs(ex)); + } + + Invalidate(); + } + + public void LoadFromData(byte[] data) + { + try + { + using var stream = new MemoryStream(data); + var bitmap = SKBitmap.Decode(stream); + if (bitmap != null) + { + Bitmap = bitmap; + } + ImageLoaded?.Invoke(this, EventArgs.Empty); + } + catch (Exception ex) + { + ImageLoadingError?.Invoke(this, new ImageLoadingErrorEventArgs(ex)); + } + } + + // Pointer event handlers + public override void OnPointerEntered(PointerEventArgs e) + { + if (!IsEnabled) return; + IsHovered = true; + SkiaVisualStateManager.GoToState(this, SkiaVisualStateManager.CommonStates.PointerOver); + Invalidate(); + } + + public override void OnPointerExited(PointerEventArgs e) + { + IsHovered = false; + if (IsPressed) + { + IsPressed = false; + } + SkiaVisualStateManager.GoToState(this, IsEnabled + ? SkiaVisualStateManager.CommonStates.Normal + : SkiaVisualStateManager.CommonStates.Disabled); + Invalidate(); + } + + public override void OnPointerPressed(PointerEventArgs e) + { + if (!IsEnabled) return; + + IsPressed = true; + SkiaVisualStateManager.GoToState(this, SkiaVisualStateManager.CommonStates.Pressed); + Invalidate(); + Pressed?.Invoke(this, EventArgs.Empty); + } + + public override void OnPointerReleased(PointerEventArgs e) + { + if (!IsEnabled) return; + + var wasPressed = IsPressed; + IsPressed = false; + SkiaVisualStateManager.GoToState(this, IsHovered + ? SkiaVisualStateManager.CommonStates.PointerOver + : SkiaVisualStateManager.CommonStates.Normal); + Invalidate(); + + Released?.Invoke(this, EventArgs.Empty); + + if (wasPressed && Bounds.Contains(new SKPoint(e.X, e.Y))) + { + Clicked?.Invoke(this, EventArgs.Empty); + } + } + + // Keyboard event handlers + public override void OnKeyDown(KeyEventArgs e) + { + if (!IsEnabled) return; + + if (e.Key == Key.Enter || e.Key == Key.Space) + { + IsPressed = true; + Invalidate(); + Pressed?.Invoke(this, EventArgs.Empty); + e.Handled = true; + } + } + + public override void OnKeyUp(KeyEventArgs e) + { + if (!IsEnabled) return; + + if (e.Key == Key.Enter || e.Key == Key.Space) + { + if (IsPressed) + { + IsPressed = false; + Invalidate(); + Released?.Invoke(this, EventArgs.Empty); + Clicked?.Invoke(this, EventArgs.Empty); + } + e.Handled = true; + } + } + + protected override SKSize MeasureOverride(SKSize availableSize) + { + var padding = new SKSize(PaddingLeft + PaddingRight, PaddingTop + PaddingBottom); + + if (_image == null) + return new SKSize(44 + padding.Width, 44 + padding.Height); // Default touch target size + + var imageWidth = _image.Width; + var imageHeight = _image.Height; + + if (availableSize.Width < float.MaxValue && availableSize.Height < float.MaxValue) + { + var availableContent = new SKSize( + availableSize.Width - padding.Width, + availableSize.Height - padding.Height); + var scale = Math.Min(availableContent.Width / imageWidth, availableContent.Height / imageHeight); + return new SKSize(imageWidth * scale + padding.Width, imageHeight * scale + padding.Height); + } + else if (availableSize.Width < float.MaxValue) + { + var availableWidth = availableSize.Width - padding.Width; + var scale = availableWidth / imageWidth; + return new SKSize(availableSize.Width, imageHeight * scale + padding.Height); + } + else if (availableSize.Height < float.MaxValue) + { + var availableHeight = availableSize.Height - padding.Height; + var scale = availableHeight / imageHeight; + return new SKSize(imageWidth * scale + padding.Width, availableSize.Height); + } + + return new SKSize(imageWidth + padding.Width, imageHeight + padding.Height); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _bitmap?.Dispose(); + _image?.Dispose(); + } + base.Dispose(disposing); + } } diff --git a/Views/SkiaIndicatorView.cs b/Views/SkiaIndicatorView.cs index 60775fa..764a6f0 100644 --- a/Views/SkiaIndicatorView.cs +++ b/Views/SkiaIndicatorView.cs @@ -1,325 +1,316 @@ -using System; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using SkiaSharp; namespace Microsoft.Maui.Platform; +/// +/// A view that displays indicators for a collection of items. +/// Used to show page indicators for CarouselView or similar controls. +/// public class SkiaIndicatorView : SkiaView { - private int _count; + private int _count = 0; + private int _position = 0; - private int _position; + /// + /// Gets or sets the number of indicators to display. + /// + public int Count + { + get => _count; + set + { + if (_count != value) + { + _count = Math.Max(0, value); + if (_position >= _count) + { + _position = Math.Max(0, _count - 1); + } + InvalidateMeasure(); + Invalidate(); + } + } + } - public int Count - { - get - { - return _count; - } - set - { - if (_count != value) - { - _count = Math.Max(0, value); - if (_position >= _count) - { - _position = Math.Max(0, _count - 1); - } - InvalidateMeasure(); - Invalidate(); - } - } - } + /// + /// Gets or sets the selected position. + /// + public int Position + { + get => _position; + set + { + int newValue = Math.Clamp(value, 0, Math.Max(0, _count - 1)); + if (_position != newValue) + { + _position = newValue; + Invalidate(); + } + } + } - public int Position - { - get - { - return _position; - } - set - { - int num = Math.Clamp(value, 0, Math.Max(0, _count - 1)); - if (_position != num) - { - _position = num; - Invalidate(); - } - } - } + /// + /// Gets or sets the indicator color. + /// + public SKColor IndicatorColor { get; set; } = new SKColor(180, 180, 180); - public SKColor IndicatorColor { get; set; } = new SKColor((byte)180, (byte)180, (byte)180); + /// + /// Gets or sets the selected indicator color. + /// + public SKColor SelectedIndicatorColor { get; set; } = new SKColor(33, 150, 243); - public SKColor SelectedIndicatorColor { get; set; } = new SKColor((byte)33, (byte)150, (byte)243); + /// + /// Gets or sets the indicator size. + /// + public float IndicatorSize { get; set; } = 10f; - public float IndicatorSize { get; set; } = 10f; + /// + /// Gets or sets the selected indicator size. + /// + public float SelectedIndicatorSize { get; set; } = 10f; - public float SelectedIndicatorSize { get; set; } = 10f; + /// + /// Gets or sets the spacing between indicators. + /// + public float IndicatorSpacing { get; set; } = 8f; - public float IndicatorSpacing { get; set; } = 8f; + /// + /// Gets or sets the indicator shape. + /// + public IndicatorShape IndicatorShape { get; set; } = IndicatorShape.Circle; - public IndicatorShape IndicatorShape { get; set; } + /// + /// Gets or sets whether indicators should have a border. + /// + public bool ShowBorder { get; set; } = false; - public bool ShowBorder { get; set; } + /// + /// Gets or sets the border color. + /// + public SKColor BorderColor { get; set; } = new SKColor(100, 100, 100); - public SKColor BorderColor { get; set; } = new SKColor((byte)100, (byte)100, (byte)100); + /// + /// Gets or sets the border width. + /// + public float BorderWidth { get; set; } = 1f; - public float BorderWidth { get; set; } = 1f; + /// + /// Gets or sets the maximum visible indicators. + /// + public int MaximumVisible { get; set; } = 10; - public int MaximumVisible { get; set; } = 10; + /// + /// Gets or sets whether to hide indicators when count is 1 or less. + /// + public bool HideSingle { get; set; } = true; - public bool HideSingle { get; set; } = true; + protected override SKSize MeasureOverride(SKSize availableSize) + { + if (_count <= 0 || (HideSingle && _count <= 1)) + { + return SKSize.Empty; + } - protected override SKSize MeasureOverride(SKSize availableSize) - { - //IL_001a: Unknown result type (might be due to invalid IL or missing references) - //IL_005a: Unknown result type (might be due to invalid IL or missing references) - if (_count <= 0 || (HideSingle && _count <= 1)) - { - return SKSize.Empty; - } - int num = Math.Min(_count, MaximumVisible); - float num2 = (float)num * IndicatorSize + (float)(num - 1) * IndicatorSpacing; - float num3 = Math.Max(IndicatorSize, SelectedIndicatorSize); - return new SKSize(num2, num3); - } + int visibleCount = Math.Min(_count, MaximumVisible); + float totalWidth = visibleCount * IndicatorSize + (visibleCount - 1) * IndicatorSpacing; + float height = Math.Max(IndicatorSize, SelectedIndicatorSize); - protected override void OnDraw(SKCanvas canvas, SKRect bounds) - { - //IL_0024: Unknown result type (might be due to invalid IL or missing references) - //IL_0059: Unknown result type (might be due to invalid IL or missing references) - //IL_005e: Unknown result type (might be due to invalid IL or missing references) - //IL_007e: Unknown result type (might be due to invalid IL or missing references) - //IL_0083: Unknown result type (might be due to invalid IL or missing references) - //IL_00eb: Unknown result type (might be due to invalid IL or missing references) - //IL_00f0: Unknown result type (might be due to invalid IL or missing references) - //IL_00f2: Unknown result type (might be due to invalid IL or missing references) - //IL_00fc: Unknown result type (might be due to invalid IL or missing references) - //IL_0103: Unknown result type (might be due to invalid IL or missing references) - //IL_010c: Expected O, but got Unknown - //IL_010c: Unknown result type (might be due to invalid IL or missing references) - //IL_0111: Unknown result type (might be due to invalid IL or missing references) - //IL_0113: Unknown result type (might be due to invalid IL or missing references) - //IL_011d: Unknown result type (might be due to invalid IL or missing references) - //IL_0124: Unknown result type (might be due to invalid IL or missing references) - //IL_012d: Expected O, but got Unknown - //IL_012d: Unknown result type (might be due to invalid IL or missing references) - //IL_0132: Unknown result type (might be due to invalid IL or missing references) - //IL_0134: Unknown result type (might be due to invalid IL or missing references) - //IL_013e: Unknown result type (might be due to invalid IL or missing references) - //IL_0145: Unknown result type (might be due to invalid IL or missing references) - //IL_0151: Unknown result type (might be due to invalid IL or missing references) - //IL_015a: Expected O, but got Unknown - if (_count <= 0 || (HideSingle && _count <= 1)) - { - return; - } - canvas.Save(); - canvas.ClipRect(base.Bounds, (SKClipOperation)1, false); - int num = Math.Min(_count, MaximumVisible); - float num2 = (float)num * IndicatorSize + (float)(num - 1) * IndicatorSpacing; - SKRect bounds2 = base.Bounds; - float num3 = ((SKRect)(ref bounds2)).MidX - num2 / 2f + IndicatorSize / 2f; - bounds2 = base.Bounds; - float midY = ((SKRect)(ref bounds2)).MidY; - int num4 = 0; - int num5 = num; - if (_count > MaximumVisible) - { - int num6 = MaximumVisible / 2; - num4 = Math.Max(0, _position - num6); - num5 = Math.Min(_count, num4 + MaximumVisible); - if (num5 == _count) - { - num4 = _count - MaximumVisible; - } - } - SKPaint val = new SKPaint - { - Color = IndicatorColor, - Style = (SKPaintStyle)0, - IsAntialias = true - }; - try - { - SKPaint val2 = new SKPaint - { - Color = SelectedIndicatorColor, - Style = (SKPaintStyle)0, - IsAntialias = true - }; - try - { - SKPaint val3 = new SKPaint - { - Color = BorderColor, - Style = (SKPaintStyle)1, - StrokeWidth = BorderWidth, - IsAntialias = true - }; - try - { - for (int i = num4; i < num5; i++) - { - int num7 = i - num4; - float x = num3 + (float)num7 * (IndicatorSize + IndicatorSpacing); - bool num8 = i == _position; - SKPaint fillPaint = (num8 ? val2 : val); - float size = (num8 ? SelectedIndicatorSize : IndicatorSize); - DrawIndicator(canvas, x, midY, size, fillPaint, val3); - } - canvas.Restore(); - } - finally - { - ((IDisposable)val3)?.Dispose(); - } - } - finally - { - ((IDisposable)val2)?.Dispose(); - } - } - finally - { - ((IDisposable)val)?.Dispose(); - } - } + return new SKSize(totalWidth, height); + } - private void DrawIndicator(SKCanvas canvas, float x, float y, float size, SKPaint fillPaint, SKPaint borderPaint) - { - //IL_005f: Unknown result type (might be due to invalid IL or missing references) - //IL_0098: Unknown result type (might be due to invalid IL or missing references) - //IL_00b6: Unknown result type (might be due to invalid IL or missing references) - //IL_00bd: Expected O, but got Unknown - //IL_0073: Unknown result type (might be due to invalid IL or missing references) - //IL_00ab: Unknown result type (might be due to invalid IL or missing references) - float num = size / 2f; - switch (IndicatorShape) - { - case IndicatorShape.Circle: - canvas.DrawCircle(x, y, num, fillPaint); - if (ShowBorder) - { - canvas.DrawCircle(x, y, num, borderPaint); - } - break; - case IndicatorShape.Square: - { - SKRect val3 = default(SKRect); - ((SKRect)(ref val3))._002Ector(x - num, y - num, x + num, y + num); - canvas.DrawRect(val3, fillPaint); - if (ShowBorder) - { - canvas.DrawRect(val3, borderPaint); - } - break; - } - case IndicatorShape.RoundedSquare: - { - SKRect val2 = default(SKRect); - ((SKRect)(ref val2))._002Ector(x - num, y - num, x + num, y + num); - float num2 = num * 0.3f; - canvas.DrawRoundRect(val2, num2, num2, fillPaint); - if (ShowBorder) - { - canvas.DrawRoundRect(val2, num2, num2, borderPaint); - } - break; - } - case IndicatorShape.Diamond: - { - SKPath val = new SKPath(); - try - { - val.MoveTo(x, y - num); - val.LineTo(x + num, y); - val.LineTo(x, y + num); - val.LineTo(x - num, y); - val.Close(); - canvas.DrawPath(val, fillPaint); - if (ShowBorder) - { - canvas.DrawPath(val, borderPaint); - } - break; - } - finally - { - ((IDisposable)val)?.Dispose(); - } - } - } - } + protected override void OnDraw(SKCanvas canvas, SKRect bounds) + { + if (_count <= 0 || (HideSingle && _count <= 1)) return; - public override SkiaView? HitTest(float x, float y) - { - //IL_0009: Unknown result type (might be due to invalid IL or missing references) - //IL_000e: Unknown result type (might be due to invalid IL or missing references) - //IL_0051: Unknown result type (might be due to invalid IL or missing references) - //IL_0056: Unknown result type (might be due to invalid IL or missing references) - if (base.IsVisible) - { - SKRect bounds = base.Bounds; - if (((SKRect)(ref bounds)).Contains(x, y)) - { - if (_count > 0) - { - int num = Math.Min(_count, MaximumVisible); - float num2 = (float)num * IndicatorSize + (float)(num - 1) * IndicatorSpacing; - bounds = base.Bounds; - float num3 = ((SKRect)(ref bounds)).MidX - num2 / 2f; - if (_count > MaximumVisible) - { - int num4 = MaximumVisible / 2; - if (Math.Max(0, _position - num4) + MaximumVisible > _count) - { - _ = _count; - _ = MaximumVisible; - } - } - for (int i = 0; i < num; i++) - { - float num5 = num3 + (float)i * (IndicatorSize + IndicatorSpacing); - if (x >= num5 && x <= num5 + IndicatorSize) - { - return this; - } - } - } - return null; - } - } - return null; - } + canvas.Save(); + canvas.ClipRect(Bounds); - public override void OnPointerPressed(PointerEventArgs e) - { - //IL_003b: Unknown result type (might be due to invalid IL or missing references) - //IL_0040: Unknown result type (might be due to invalid IL or missing references) - if (!base.IsEnabled || _count <= 0) - { - return; - } - int num = Math.Min(_count, MaximumVisible); - float num2 = (float)num * IndicatorSize + (float)(num - 1) * IndicatorSpacing; - SKRect bounds = base.Bounds; - float num3 = ((SKRect)(ref bounds)).MidX - num2 / 2f; - int num4 = 0; - if (_count > MaximumVisible) - { - int num5 = MaximumVisible / 2; - num4 = Math.Max(0, _position - num5); - if (num4 + MaximumVisible > _count) - { - num4 = _count - MaximumVisible; - } - } - int num6 = (int)((e.X - num3) / (IndicatorSize + IndicatorSpacing)); - if (num6 >= 0 && num6 < num) - { - Position = num4 + num6; - e.Handled = true; - } - base.OnPointerPressed(e); - } + int visibleCount = Math.Min(_count, MaximumVisible); + float totalWidth = visibleCount * IndicatorSize + (visibleCount - 1) * IndicatorSpacing; + float startX = Bounds.MidX - totalWidth / 2 + IndicatorSize / 2; + float centerY = Bounds.MidY; + + // Determine visible range if count > MaximumVisible + int startIndex = 0; + int endIndex = visibleCount; + + if (_count > MaximumVisible) + { + int halfVisible = MaximumVisible / 2; + startIndex = Math.Max(0, _position - halfVisible); + endIndex = Math.Min(_count, startIndex + MaximumVisible); + if (endIndex == _count) + { + startIndex = _count - MaximumVisible; + } + } + + using var normalPaint = new SKPaint + { + Color = IndicatorColor, + Style = SKPaintStyle.Fill, + IsAntialias = true + }; + + using var selectedPaint = new SKPaint + { + Color = SelectedIndicatorColor, + Style = SKPaintStyle.Fill, + IsAntialias = true + }; + + using var borderPaint = new SKPaint + { + Color = BorderColor, + Style = SKPaintStyle.Stroke, + StrokeWidth = BorderWidth, + IsAntialias = true + }; + + for (int i = startIndex; i < endIndex; i++) + { + int visualIndex = i - startIndex; + float x = startX + visualIndex * (IndicatorSize + IndicatorSpacing); + bool isSelected = i == _position; + + var paint = isSelected ? selectedPaint : normalPaint; + float size = isSelected ? SelectedIndicatorSize : IndicatorSize; + + DrawIndicator(canvas, x, centerY, size, paint, borderPaint); + } + + canvas.Restore(); + } + + private void DrawIndicator(SKCanvas canvas, float x, float y, float size, SKPaint fillPaint, SKPaint borderPaint) + { + float radius = size / 2; + + switch (IndicatorShape) + { + case IndicatorShape.Circle: + canvas.DrawCircle(x, y, radius, fillPaint); + if (ShowBorder) + { + canvas.DrawCircle(x, y, radius, borderPaint); + } + break; + + case IndicatorShape.Square: + var rect = new SKRect(x - radius, y - radius, x + radius, y + radius); + canvas.DrawRect(rect, fillPaint); + if (ShowBorder) + { + canvas.DrawRect(rect, borderPaint); + } + break; + + case IndicatorShape.RoundedSquare: + var roundRect = new SKRect(x - radius, y - radius, x + radius, y + radius); + float cornerRadius = radius * 0.3f; + canvas.DrawRoundRect(roundRect, cornerRadius, cornerRadius, fillPaint); + if (ShowBorder) + { + canvas.DrawRoundRect(roundRect, cornerRadius, cornerRadius, borderPaint); + } + break; + + case IndicatorShape.Diamond: + using (var path = new SKPath()) + { + path.MoveTo(x, y - radius); + path.LineTo(x + radius, y); + path.LineTo(x, y + radius); + path.LineTo(x - radius, y); + path.Close(); + canvas.DrawPath(path, fillPaint); + if (ShowBorder) + { + canvas.DrawPath(path, borderPaint); + } + } + break; + } + } + + public override SkiaView? HitTest(float x, float y) + { + if (!IsVisible || !Bounds.Contains(x, y)) return null; + + // Check if click is on an indicator + if (_count > 0) + { + int visibleCount = Math.Min(_count, MaximumVisible); + float totalWidth = visibleCount * IndicatorSize + (visibleCount - 1) * IndicatorSpacing; + float startX = Bounds.MidX - totalWidth / 2; + + int startIndex = 0; + if (_count > MaximumVisible) + { + int halfVisible = MaximumVisible / 2; + startIndex = Math.Max(0, _position - halfVisible); + if (startIndex + MaximumVisible > _count) + { + startIndex = _count - MaximumVisible; + } + } + + for (int i = 0; i < visibleCount; i++) + { + float indicatorX = startX + i * (IndicatorSize + IndicatorSpacing); + if (x >= indicatorX && x <= indicatorX + IndicatorSize) + { + return this; + } + } + } + + return null; + } + + public override void OnPointerPressed(PointerEventArgs e) + { + if (!IsEnabled || _count <= 0) return; + + // Calculate which indicator was clicked + int visibleCount = Math.Min(_count, MaximumVisible); + float totalWidth = visibleCount * IndicatorSize + (visibleCount - 1) * IndicatorSpacing; + float startX = Bounds.MidX - totalWidth / 2; + + int startIndex = 0; + if (_count > MaximumVisible) + { + int halfVisible = MaximumVisible / 2; + startIndex = Math.Max(0, _position - halfVisible); + if (startIndex + MaximumVisible > _count) + { + startIndex = _count - MaximumVisible; + } + } + + float relativeX = e.X - startX; + int visualIndex = (int)(relativeX / (IndicatorSize + IndicatorSpacing)); + + if (visualIndex >= 0 && visualIndex < visibleCount) + { + Position = startIndex + visualIndex; + e.Handled = true; + } + + base.OnPointerPressed(e); + } +} + +/// +/// Shape of indicator dots. +/// +public enum IndicatorShape +{ + Circle, + Square, + RoundedSquare, + Diamond } diff --git a/Views/SkiaItemsView.cs b/Views/SkiaItemsView.cs index ea80dd9..f07667a 100644 --- a/Views/SkiaItemsView.cs +++ b/Views/SkiaItemsView.cs @@ -1,872 +1,755 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Collections.Specialized; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using SkiaSharp; +using System.Collections; +using System.Collections.Specialized; +using Microsoft.Maui.Graphics; namespace Microsoft.Maui.Platform; +/// +/// Base class for Skia-rendered items views (CollectionView, ListView). +/// Provides item rendering, scrolling, and virtualization. +/// public class SkiaItemsView : SkiaView { - private IEnumerable? _itemsSource; + private IEnumerable? _itemsSource; + private List _items = new(); + protected float _scrollOffset; + private float _itemHeight = 44; // Default item height + private float _itemSpacing = 0; + private int _firstVisibleIndex; + private int _lastVisibleIndex; + private bool _isDragging; + private bool _isDraggingScrollbar; + private float _dragStartY; + private float _dragStartOffset; + private float _scrollbarDragStartY; + private float _scrollbarDragStartScrollOffset; + private float _scrollbarDragAvailableTrack; + private float _scrollbarDragMaxScroll; + private float _velocity; + private DateTime _lastDragTime; - private List _items = new List(); + // Scroll bar + private bool _showVerticalScrollBar = true; + private float _scrollBarWidth = 8; + private SKColor _scrollBarColor = new SKColor(128, 128, 128, 128); + private SKColor _scrollBarTrackColor = new SKColor(200, 200, 200, 64); - protected float _scrollOffset; + public IEnumerable? ItemsSource + { + get => _itemsSource; + set + { + if (_itemsSource is INotifyCollectionChanged oldCollection) + { + oldCollection.CollectionChanged -= OnCollectionChanged; + } - private float _itemHeight = 44f; + _itemsSource = value; + RefreshItems(); - private float _itemSpacing; + if (_itemsSource is INotifyCollectionChanged newCollection) + { + newCollection.CollectionChanged += OnCollectionChanged; + } - private int _firstVisibleIndex; + Invalidate(); + } + } - private int _lastVisibleIndex; + public float ItemHeight + { + get => _itemHeight; + set + { + _itemHeight = value; + Invalidate(); + } + } - private bool _isDragging; + public float ItemSpacing + { + get => _itemSpacing; + set + { + _itemSpacing = value; + Invalidate(); + } + } - private bool _isDraggingScrollbar; + public ScrollBarVisibility VerticalScrollBarVisibility { get; set; } = ScrollBarVisibility.Default; + public ScrollBarVisibility HorizontalScrollBarVisibility { get; set; } = ScrollBarVisibility.Never; - private float _dragStartY; + public object? EmptyView { get; set; } + public string? EmptyViewText { get; set; } = "No items"; - private float _dragStartOffset; + // Item rendering delegate (legacy) + public Func? ItemRenderer { get; set; } - private float _scrollbarDragStartY; + // Item view creator - creates SkiaView from data item using DataTemplate + public Func? ItemViewCreator { get; set; } - private float _scrollbarDragStartScrollOffset; + // Cache of created item views for virtualization + protected readonly Dictionary _itemViewCache = new(); - private float _scrollbarDragAvailableTrack; + // Cache of individual item heights for variable height items + protected readonly Dictionary _itemHeights = new(); - private float _scrollbarDragMaxScroll; + // Track last measured width to clear cache when width changes + private float _lastMeasuredWidth = 0; - private float _velocity; + // Selection support (overridden in SkiaCollectionView) + public virtual int SelectedIndex { get; set; } = -1; - private DateTime _lastDragTime; + public event EventHandler? Scrolled; + public event EventHandler? ItemTapped; - private bool _showVerticalScrollBar = true; + public SkiaItemsView() + { + IsFocusable = true; + } - private float _scrollBarWidth = 8f; + protected virtual void RefreshItems() + { + Console.WriteLine($"[SkiaItemsView] RefreshItems called, clearing {_items.Count} items and {_itemViewCache.Count} cached views"); + _items.Clear(); + _itemViewCache.Clear(); // Clear cached views when items change + _itemHeights.Clear(); // Clear cached heights + if (_itemsSource != null) + { + foreach (var item in _itemsSource) + { + _items.Add(item); + } + } + Console.WriteLine($"[SkiaItemsView] RefreshItems done, now have {_items.Count} items"); + _scrollOffset = 0; + } - private SKColor _scrollBarColor = new SKColor((byte)128, (byte)128, (byte)128, (byte)128); + private void OnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + RefreshItems(); + Invalidate(); + } - private SKColor _scrollBarTrackColor = new SKColor((byte)200, (byte)200, (byte)200, (byte)64); + /// + /// Gets the height for a specific item, using cached height or default. + /// + protected float GetItemHeight(int index) + { + return _itemHeights.TryGetValue(index, out var height) ? height : _itemHeight; + } - protected readonly Dictionary _itemViewCache = new Dictionary(); + /// + /// Gets the Y offset for a specific item (cumulative height of all previous items). + /// + protected float GetItemOffset(int index) + { + float offset = 0; + for (int i = 0; i < index && i < _items.Count; i++) + { + offset += GetItemHeight(i) + _itemSpacing; + } + return offset; + } - protected readonly Dictionary _itemHeights = new Dictionary(); + /// + /// Calculates total content height based on individual item heights. + /// + protected float TotalContentHeight + { + get + { + if (_items.Count == 0) return 0; - private float _lastMeasuredWidth; + float total = 0; + for (int i = 0; i < _items.Count; i++) + { + total += GetItemHeight(i); + if (i < _items.Count - 1) total += _itemSpacing; + } + return total; + } + } - public IEnumerable? ItemsSource - { - get - { - return _itemsSource; - } - set - { - if (_itemsSource is INotifyCollectionChanged notifyCollectionChanged) - { - notifyCollectionChanged.CollectionChanged -= OnCollectionChanged; - } - _itemsSource = value; - RefreshItems(); - if (_itemsSource is INotifyCollectionChanged notifyCollectionChanged2) - { - notifyCollectionChanged2.CollectionChanged += OnCollectionChanged; - } - Invalidate(); - } - } + // Use ScreenBounds.Height for visible viewport + protected float MaxScrollOffset => Math.Max(0, TotalContentHeight - ScreenBounds.Height); - public float ItemHeight - { - get - { - return _itemHeight; - } - set - { - _itemHeight = value; - Invalidate(); - } - } + protected override void OnDraw(SKCanvas canvas, SKRect bounds) + { + Console.WriteLine($"[SkiaItemsView] OnDraw - bounds={bounds}, items={_items.Count}, ItemViewCreator={(ItemViewCreator != null ? "set" : "null")}"); - public float ItemSpacing - { - get - { - return _itemSpacing; - } - set - { - _itemSpacing = value; - Invalidate(); - } - } + // Draw background + if (BackgroundColor != SKColors.Transparent) + { + using var bgPaint = new SKPaint + { + Color = BackgroundColor, + Style = SKPaintStyle.Fill + }; + canvas.DrawRect(bounds, bgPaint); + } - public ScrollBarVisibility VerticalScrollBarVisibility { get; set; } + // If no items, show empty view + if (_items.Count == 0) + { + DrawEmptyView(canvas, bounds); + return; + } - public ScrollBarVisibility HorizontalScrollBarVisibility { get; set; } = ScrollBarVisibility.Never; + // Find first visible index by walking through items + _firstVisibleIndex = 0; + float cumulativeOffset = 0; + for (int i = 0; i < _items.Count; i++) + { + var itemH = GetItemHeight(i); + if (cumulativeOffset + itemH > _scrollOffset) + { + _firstVisibleIndex = i; + break; + } + cumulativeOffset += itemH + _itemSpacing; + } - public object? EmptyView { get; set; } + // Clip to bounds + canvas.Save(); + canvas.ClipRect(bounds); - public string? EmptyViewText { get; set; } = "No items"; + // Draw visible items using variable heights + using var paint = new SKPaint + { + IsAntialias = true + }; - public Func? ItemRenderer { get; set; } + float currentY = bounds.Top + GetItemOffset(_firstVisibleIndex) - _scrollOffset; + for (int i = _firstVisibleIndex; i < _items.Count; i++) + { + var itemH = GetItemHeight(i); + var itemRect = new SKRect(bounds.Left, currentY, bounds.Right - (_showVerticalScrollBar ? _scrollBarWidth : 0), currentY + itemH); - public Func? ItemViewCreator { get; set; } + // Stop if we've passed the visible area + if (itemRect.Top > bounds.Bottom) + { + _lastVisibleIndex = i - 1; + break; + } - public virtual int SelectedIndex { get; set; } = -1; + _lastVisibleIndex = i; - protected float TotalContentHeight - { - get - { - if (_items.Count == 0) - { - return 0f; - } - float num = 0f; - for (int i = 0; i < _items.Count; i++) - { - num += GetItemHeight(i); - if (i < _items.Count - 1) - { - num += _itemSpacing; - } - } - return num; - } - } + if (itemRect.Bottom >= bounds.Top) + { + DrawItem(canvas, _items[i], i, itemRect, paint); + } - protected float MaxScrollOffset - { - get - { - //IL_000c: Unknown result type (might be due to invalid IL or missing references) - //IL_0011: Unknown result type (might be due to invalid IL or missing references) - float totalContentHeight = TotalContentHeight; - SKRect screenBounds = base.ScreenBounds; - return Math.Max(0f, totalContentHeight - ((SKRect)(ref screenBounds)).Height); - } - } + currentY += itemH + _itemSpacing; + } - protected int ItemCount => _items.Count; + canvas.Restore(); - public event EventHandler? Scrolled; + // Draw scrollbar + if (_showVerticalScrollBar && TotalContentHeight > bounds.Height) + { + DrawScrollBar(canvas, bounds); + } + } - public event EventHandler? ItemTapped; + protected virtual void DrawItem(SKCanvas canvas, object item, int index, SKRect bounds, SKPaint paint) + { + // Draw selection highlight + if (index == SelectedIndex) + { + paint.Color = new SKColor(0x21, 0x96, 0xF3, 0x59); // Light blue with 35% opacity + paint.Style = SKPaintStyle.Fill; + canvas.DrawRect(bounds, paint); + } - public SkiaItemsView() - { - //IL_003d: Unknown result type (might be due to invalid IL or missing references) - //IL_0042: Unknown result type (might be due to invalid IL or missing references) - //IL_0059: Unknown result type (might be due to invalid IL or missing references) - //IL_005e: Unknown result type (might be due to invalid IL or missing references) - base.IsFocusable = true; - } + // Try to use ItemViewCreator for templated rendering + if (ItemViewCreator != null) + { + Console.WriteLine($"[SkiaItemsView] DrawItem {index} - ItemViewCreator exists, item: {item}"); + // Get or create cached view for this index + if (!_itemViewCache.TryGetValue(index, out var itemView) || itemView == null) + { + itemView = ItemViewCreator(item); + if (itemView != null) + { + itemView.Parent = this; + _itemViewCache[index] = itemView; + } + } - protected virtual void RefreshItems() - { - Console.WriteLine($"[SkiaItemsView] RefreshItems called, clearing {_items.Count} items and {_itemViewCache.Count} cached views"); - _items.Clear(); - _itemViewCache.Clear(); - _itemHeights.Clear(); - if (_itemsSource != null) - { - foreach (object item in _itemsSource) - { - _items.Add(item); - } - } - Console.WriteLine($"[SkiaItemsView] RefreshItems done, now have {_items.Count} items"); - _scrollOffset = 0f; - } + if (itemView != null) + { + // Measure with large height to get natural size + var availableSize = new SKSize(bounds.Width, float.MaxValue); + var measuredSize = itemView.Measure(availableSize); - private void OnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) - { - RefreshItems(); - Invalidate(); - } + // Store individual item height (with minimum of default height) + var measuredHeight = Math.Max(measuredSize.Height, _itemHeight); + if (!_itemHeights.TryGetValue(index, out var cachedHeight) || Math.Abs(cachedHeight - measuredHeight) > 1) + { + _itemHeights[index] = measuredHeight; + // Request redraw if height changed significantly + if (Math.Abs(cachedHeight - measuredHeight) > 5) + { + Invalidate(); + } + } - protected float GetItemHeight(int index) - { - if (!_itemHeights.TryGetValue(index, out var value)) - { - return _itemHeight; - } - return value; - } + // Arrange with the actual measured height + var actualBounds = new SKRect(bounds.Left, bounds.Top, bounds.Right, bounds.Top + measuredHeight); + itemView.Arrange(actualBounds); + itemView.Draw(canvas); + return; + } + } + else + { + Console.WriteLine($"[SkiaItemsView] DrawItem {index} - ItemViewCreator is NULL, falling back to ToString"); + } - protected float GetItemOffset(int index) - { - float num = 0f; - for (int i = 0; i < index && i < _items.Count; i++) - { - num += GetItemHeight(i) + _itemSpacing; - } - return num; - } + // Draw separator + paint.Color = new SKColor(0xE0, 0xE0, 0xE0); + paint.Style = SKPaintStyle.Stroke; + paint.StrokeWidth = 1; + canvas.DrawLine(bounds.Left, bounds.Bottom, bounds.Right, bounds.Bottom, paint); - protected override void OnDraw(SKCanvas canvas, SKRect bounds) - { - //IL_0018: Unknown result type (might be due to invalid IL or missing references) - //IL_0070: Unknown result type (might be due to invalid IL or missing references) - //IL_0075: Unknown result type (might be due to invalid IL or missing references) - //IL_0081: Unknown result type (might be due to invalid IL or missing references) - //IL_0086: Unknown result type (might be due to invalid IL or missing references) - //IL_0088: Unknown result type (might be due to invalid IL or missing references) - //IL_0092: Unknown result type (might be due to invalid IL or missing references) - //IL_009b: Expected O, but got Unknown - //IL_00c1: Unknown result type (might be due to invalid IL or missing references) - //IL_009c: Unknown result type (might be due to invalid IL or missing references) - //IL_0123: Unknown result type (might be due to invalid IL or missing references) - //IL_012b: Unknown result type (might be due to invalid IL or missing references) - //IL_0130: Unknown result type (might be due to invalid IL or missing references) - //IL_0138: Expected O, but got Unknown - //IL_022b: Unknown result type (might be due to invalid IL or missing references) - //IL_01e0: Unknown result type (might be due to invalid IL or missing references) - Console.WriteLine($"[SkiaItemsView] OnDraw - bounds={bounds}, items={_items.Count}, ItemViewCreator={((ItemViewCreator != null) ? "set" : "null")}"); - if (base.BackgroundColor != SKColors.Transparent) - { - SKPaint val = new SKPaint - { - Color = base.BackgroundColor, - Style = (SKPaintStyle)0 - }; - try - { - canvas.DrawRect(bounds, val); - } - finally - { - ((IDisposable)val)?.Dispose(); - } - } - if (_items.Count == 0) - { - DrawEmptyView(canvas, bounds); - return; - } - _firstVisibleIndex = 0; - float num = 0f; - for (int i = 0; i < _items.Count; i++) - { - float itemHeight = GetItemHeight(i); - if (num + itemHeight > _scrollOffset) - { - _firstVisibleIndex = i; - break; - } - num += itemHeight + _itemSpacing; - } - canvas.Save(); - canvas.ClipRect(bounds, (SKClipOperation)1, false); - SKPaint val2 = new SKPaint - { - IsAntialias = true - }; - try - { - float num2 = ((SKRect)(ref bounds)).Top + GetItemOffset(_firstVisibleIndex) - _scrollOffset; - SKRect bounds2 = default(SKRect); - for (int j = _firstVisibleIndex; j < _items.Count; j++) - { - float itemHeight2 = GetItemHeight(j); - ((SKRect)(ref bounds2))._002Ector(((SKRect)(ref bounds)).Left, num2, ((SKRect)(ref bounds)).Right - (_showVerticalScrollBar ? _scrollBarWidth : 0f), num2 + itemHeight2); - if (((SKRect)(ref bounds2)).Top > ((SKRect)(ref bounds)).Bottom) - { - _lastVisibleIndex = j - 1; - break; - } - _lastVisibleIndex = j; - if (((SKRect)(ref bounds2)).Bottom >= ((SKRect)(ref bounds)).Top) - { - DrawItem(canvas, _items[j], j, bounds2, val2); - } - num2 += itemHeight2 + _itemSpacing; - } - canvas.Restore(); - if (_showVerticalScrollBar && TotalContentHeight > ((SKRect)(ref bounds)).Height) - { - DrawScrollBar(canvas, bounds); - } - } - finally - { - ((IDisposable)val2)?.Dispose(); - } - } + // Use custom renderer if provided + if (ItemRenderer != null) + { + if (ItemRenderer(item, index, bounds, canvas, paint)) + return; + } - protected virtual void DrawItem(SKCanvas canvas, object item, int index, SKRect bounds, SKPaint paint) - { - //IL_0019: Unknown result type (might be due to invalid IL or missing references) - //IL_002c: Unknown result type (might be due to invalid IL or missing references) - //IL_01b8: Unknown result type (might be due to invalid IL or missing references) - //IL_0219: Unknown result type (might be due to invalid IL or missing references) - //IL_023f: Unknown result type (might be due to invalid IL or missing references) - //IL_0245: Expected O, but got Unknown - //IL_020a: Unknown result type (might be due to invalid IL or missing references) - //IL_0246: Unknown result type (might be due to invalid IL or missing references) - //IL_024b: Unknown result type (might be due to invalid IL or missing references) - //IL_024c: Unknown result type (might be due to invalid IL or missing references) - //IL_0256: Unknown result type (might be due to invalid IL or missing references) - //IL_025e: Expected O, but got Unknown - //IL_00d6: Unknown result type (might be due to invalid IL or missing references) - //IL_00d8: Unknown result type (might be due to invalid IL or missing references) - //IL_00dd: Unknown result type (might be due to invalid IL or missing references) - //IL_0161: Unknown result type (might be due to invalid IL or missing references) - //IL_0276: Unknown result type (might be due to invalid IL or missing references) - if (index == SelectedIndex) - { - paint.Color = new SKColor((byte)33, (byte)150, (byte)243, (byte)89); - paint.Style = (SKPaintStyle)0; - canvas.DrawRect(bounds, paint); - } - if (ItemViewCreator != null) - { - Console.WriteLine($"[SkiaItemsView] DrawItem {index} - ItemViewCreator exists, item: {item}"); - if (!_itemViewCache.TryGetValue(index, out SkiaView value) || value == null) - { - value = ItemViewCreator(item); - if (value != null) - { - value.Parent = this; - _itemViewCache[index] = value; - } - } - if (value != null) - { - SKSize availableSize = default(SKSize); - ((SKSize)(ref availableSize))._002Ector(((SKRect)(ref bounds)).Width, float.MaxValue); - SKSize val = value.Measure(availableSize); - float num = Math.Max(((SKSize)(ref val)).Height, _itemHeight); - if (!_itemHeights.TryGetValue(index, out var value2) || Math.Abs(value2 - num) > 1f) - { - _itemHeights[index] = num; - if (Math.Abs(value2 - num) > 5f) - { - Invalidate(); - } - } - SKRect bounds2 = default(SKRect); - ((SKRect)(ref bounds2))._002Ector(((SKRect)(ref bounds)).Left, ((SKRect)(ref bounds)).Top, ((SKRect)(ref bounds)).Right, ((SKRect)(ref bounds)).Top + num); - value.Arrange(bounds2); - value.Draw(canvas); - return; - } - } - else - { - Console.WriteLine($"[SkiaItemsView] DrawItem {index} - ItemViewCreator is NULL, falling back to ToString"); - } - paint.Color = new SKColor((byte)224, (byte)224, (byte)224); - paint.Style = (SKPaintStyle)1; - paint.StrokeWidth = 1f; - canvas.DrawLine(((SKRect)(ref bounds)).Left, ((SKRect)(ref bounds)).Bottom, ((SKRect)(ref bounds)).Right, ((SKRect)(ref bounds)).Bottom, paint); - if (ItemRenderer != null && ItemRenderer(item, index, bounds, canvas, paint)) - { - return; - } - paint.Color = SKColors.Black; - paint.Style = (SKPaintStyle)0; - SKFont val2 = new SKFont(SKTypeface.Default, 14f, 1f, 0f); - try - { - SKPaint val3 = new SKPaint(val2) - { - Color = SKColors.Black, - IsAntialias = true - }; - try - { - string text = item?.ToString() ?? ""; - SKRect val4 = default(SKRect); - val3.MeasureText(text, ref val4); - float num2 = ((SKRect)(ref bounds)).Left + 16f; - float num3 = ((SKRect)(ref bounds)).MidY - ((SKRect)(ref val4)).MidY; - canvas.DrawText(text, num2, num3, val3); - } - finally - { - ((IDisposable)val3)?.Dispose(); - } - } - finally - { - ((IDisposable)val2)?.Dispose(); - } - } + // Default rendering - just show ToString + paint.Color = SKColors.Black; + paint.Style = SKPaintStyle.Fill; - protected virtual void DrawEmptyView(SKCanvas canvas, SKRect bounds) - { - //IL_0000: Unknown result type (might be due to invalid IL or missing references) - //IL_0005: Unknown result type (might be due to invalid IL or missing references) - //IL_0015: Unknown result type (might be due to invalid IL or missing references) - //IL_001f: Unknown result type (might be due to invalid IL or missing references) - //IL_0027: Expected O, but got Unknown - //IL_003b: Unknown result type (might be due to invalid IL or missing references) - //IL_0041: Expected O, but got Unknown - //IL_0042: Unknown result type (might be due to invalid IL or missing references) - //IL_0047: Unknown result type (might be due to invalid IL or missing references) - //IL_0057: Unknown result type (might be due to invalid IL or missing references) - //IL_0061: Unknown result type (might be due to invalid IL or missing references) - //IL_0069: Expected O, but got Unknown - //IL_007b: Unknown result type (might be due to invalid IL or missing references) - SKPaint val = new SKPaint - { - Color = new SKColor((byte)128, (byte)128, (byte)128), - IsAntialias = true - }; - try - { - SKFont val2 = new SKFont(SKTypeface.Default, 16f, 1f, 0f); - try - { - SKPaint val3 = new SKPaint(val2) - { - Color = new SKColor((byte)128, (byte)128, (byte)128), - IsAntialias = true - }; - try - { - string text = EmptyViewText ?? "No items"; - SKRect val4 = default(SKRect); - val3.MeasureText(text, ref val4); - float num = ((SKRect)(ref bounds)).MidX - ((SKRect)(ref val4)).MidX; - float num2 = ((SKRect)(ref bounds)).MidY - ((SKRect)(ref val4)).MidY; - canvas.DrawText(text, num, num2, val3); - } - finally - { - ((IDisposable)val3)?.Dispose(); - } - } - finally - { - ((IDisposable)val2)?.Dispose(); - } - } - finally - { - ((IDisposable)val)?.Dispose(); - } - } + using var font = new SKFont(SKTypeface.Default, 14); + using var textPaint = new SKPaint(font) + { + Color = SKColors.Black, + IsAntialias = true + }; - private void DrawScrollBar(SKCanvas canvas, SKRect bounds) - { - //IL_002a: Unknown result type (might be due to invalid IL or missing references) - //IL_002f: Unknown result type (might be due to invalid IL or missing references) - //IL_0031: Unknown result type (might be due to invalid IL or missing references) - //IL_003b: Unknown result type (might be due to invalid IL or missing references) - //IL_0043: Expected O, but got Unknown - //IL_0044: Unknown result type (might be due to invalid IL or missing references) - //IL_00ba: Unknown result type (might be due to invalid IL or missing references) - //IL_00bf: Unknown result type (might be due to invalid IL or missing references) - //IL_00c1: Unknown result type (might be due to invalid IL or missing references) - //IL_00c6: Unknown result type (might be due to invalid IL or missing references) - //IL_00c8: Unknown result type (might be due to invalid IL or missing references) - //IL_00d2: Unknown result type (might be due to invalid IL or missing references) - //IL_00d9: Unknown result type (might be due to invalid IL or missing references) - //IL_00e2: Expected O, but got Unknown - //IL_00f7: Unknown result type (might be due to invalid IL or missing references) - //IL_00fb: Unknown result type (might be due to invalid IL or missing references) - //IL_0107: Expected O, but got Unknown - SKRect val = default(SKRect); - ((SKRect)(ref val))._002Ector(((SKRect)(ref bounds)).Right - _scrollBarWidth, ((SKRect)(ref bounds)).Top, ((SKRect)(ref bounds)).Right, ((SKRect)(ref bounds)).Bottom); - SKPaint val2 = new SKPaint - { - Color = _scrollBarTrackColor, - Style = (SKPaintStyle)0 - }; - try - { - canvas.DrawRect(val, val2); - float num = ((SKRect)(ref bounds)).Height / TotalContentHeight; - float num2 = Math.Max(20f, ((SKRect)(ref bounds)).Height * num); - float num3 = _scrollOffset / MaxScrollOffset; - float num4 = ((SKRect)(ref bounds)).Top + (((SKRect)(ref bounds)).Height - num2) * num3; - SKRect val3 = new SKRect(((SKRect)(ref bounds)).Right - _scrollBarWidth + 1f, num4, ((SKRect)(ref bounds)).Right - 1f, num4 + num2); - SKPaint val4 = new SKPaint - { - Color = _scrollBarColor, - Style = (SKPaintStyle)0, - IsAntialias = true - }; - try - { - float num5 = (_scrollBarWidth - 2f) / 2f; - canvas.DrawRoundRect(new SKRoundRect(val3, num5), val4); - } - finally - { - ((IDisposable)val4)?.Dispose(); - } - } - finally - { - ((IDisposable)val2)?.Dispose(); - } - } + var text = item?.ToString() ?? ""; + var textBounds = new SKRect(); + textPaint.MeasureText(text, ref textBounds); - public override void OnPointerPressed(PointerEventArgs e) - { - //IL_004b: Unknown result type (might be due to invalid IL or missing references) - //IL_0064: Unknown result type (might be due to invalid IL or missing references) - //IL_00b3: Unknown result type (might be due to invalid IL or missing references) - //IL_00b8: Unknown result type (might be due to invalid IL or missing references) - //IL_00c6: Unknown result type (might be due to invalid IL or missing references) - //IL_00cb: Unknown result type (might be due to invalid IL or missing references) - //IL_0106: Unknown result type (might be due to invalid IL or missing references) - //IL_010b: Unknown result type (might be due to invalid IL or missing references) - //IL_0114: Unknown result type (might be due to invalid IL or missing references) - //IL_0119: Unknown result type (might be due to invalid IL or missing references) - //IL_0131: Unknown result type (might be due to invalid IL or missing references) - //IL_0136: Unknown result type (might be due to invalid IL or missing references) - Console.WriteLine($"[SkiaItemsView] OnPointerPressed - x={e.X}, y={e.Y}, Bounds={base.Bounds}, ScreenBounds={base.ScreenBounds}, ItemCount={_items.Count}"); - if (!base.IsEnabled) - { - return; - } - if (_showVerticalScrollBar) - { - float totalContentHeight = TotalContentHeight; - SKRect bounds = base.Bounds; - if (totalContentHeight > ((SKRect)(ref bounds)).Height) - { - SKRect scrollbarThumbBounds = GetScrollbarThumbBounds(); - if (((SKRect)(ref scrollbarThumbBounds)).Contains(e.X, e.Y)) - { - _isDraggingScrollbar = true; - _scrollbarDragStartY = e.Y; - _scrollbarDragStartScrollOffset = _scrollOffset; - bounds = base.Bounds; - float height = ((SKRect)(ref bounds)).Height; - bounds = base.Bounds; - float num = Math.Max(20f, height * (((SKRect)(ref bounds)).Height / TotalContentHeight)); - bounds = base.Bounds; - _scrollbarDragAvailableTrack = ((SKRect)(ref bounds)).Height - num; - _scrollbarDragMaxScroll = MaxScrollOffset; - return; - } - } - } - _isDragging = true; - _dragStartY = e.Y; - _dragStartOffset = _scrollOffset; - _lastDragTime = DateTime.Now; - _velocity = 0f; - } + var x = bounds.Left + 16; + var y = bounds.MidY - textBounds.MidY; + canvas.DrawText(text, x, y, textPaint); + } - private SKRect GetScrollbarThumbBounds() - { - //IL_0001: Unknown result type (might be due to invalid IL or missing references) - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - //IL_007c: Unknown result type (might be due to invalid IL or missing references) - SKRect screenBounds = base.ScreenBounds; - float num = ((SKRect)(ref screenBounds)).Height / TotalContentHeight; - float num2 = Math.Max(20f, ((SKRect)(ref screenBounds)).Height * num); - float num3 = ((MaxScrollOffset > 0f) ? (_scrollOffset / MaxScrollOffset) : 0f); - float num4 = ((SKRect)(ref screenBounds)).Top + (((SKRect)(ref screenBounds)).Height - num2) * num3; - return new SKRect(((SKRect)(ref screenBounds)).Right - _scrollBarWidth, num4, ((SKRect)(ref screenBounds)).Right, num4 + num2); - } + protected virtual void DrawEmptyView(SKCanvas canvas, SKRect bounds) + { + using var paint = new SKPaint + { + Color = new SKColor(0x80, 0x80, 0x80), + IsAntialias = true + }; - public override void OnPointerMoved(PointerEventArgs e) - { - if (_isDraggingScrollbar) - { - if (_scrollbarDragAvailableTrack > 0f) - { - float num = (e.Y - _scrollbarDragStartY) / _scrollbarDragAvailableTrack * _scrollbarDragMaxScroll; - SetScrollOffset(_scrollbarDragStartScrollOffset + num); - } - } - else if (_isDragging) - { - float num2 = _dragStartY - e.Y; - float num3 = _dragStartOffset + num2; - DateTime now = DateTime.Now; - double totalSeconds = (now - _lastDragTime).TotalSeconds; - if (totalSeconds > 0.0) - { - _velocity = (float)((double)(_scrollOffset - num3) / totalSeconds); - } - _lastDragTime = now; - SetScrollOffset(num3); - } - } + using var font = new SKFont(SKTypeface.Default, 16); + using var textPaint = new SKPaint(font) + { + Color = new SKColor(0x80, 0x80, 0x80), + IsAntialias = true + }; - public override void OnPointerReleased(PointerEventArgs e) - { - //IL_003f: Unknown result type (might be due to invalid IL or missing references) - //IL_0044: Unknown result type (might be due to invalid IL or missing references) - if (_isDraggingScrollbar) - { - _isDraggingScrollbar = false; - } - else - { - if (!_isDragging) - { - return; - } - _isDragging = false; - if (!(Math.Abs(e.Y - _dragStartY) < 5f)) - { - return; - } - SKRect screenBounds = base.ScreenBounds; - float num = e.Y - ((SKRect)(ref screenBounds)).Top + _scrollOffset; - int num2 = -1; - float num3 = 0f; - for (int i = 0; i < _items.Count; i++) - { - float itemHeight = GetItemHeight(i); - if (num >= num3 && num < num3 + itemHeight) - { - num2 = i; - break; - } - num3 += itemHeight + _itemSpacing; - } - Console.WriteLine($"[SkiaItemsView] Tap at Y={e.Y}, screenBounds.Top={((SKRect)(ref screenBounds)).Top}, scrollOffset={_scrollOffset}, localY={num}, index={num2}"); - if (num2 >= 0 && num2 < _items.Count) - { - OnItemTapped(num2, _items[num2]); - } - } - } + var text = EmptyViewText ?? "No items"; + var textBounds = new SKRect(); + textPaint.MeasureText(text, ref textBounds); - private float GetTotalParentScrollY() - { - float num = 0f; - for (SkiaView parent = base.Parent; parent != null; parent = parent.Parent) - { - if (parent is SkiaScrollView skiaScrollView) - { - num += skiaScrollView.ScrollY; - } - } - return num; - } + var x = bounds.MidX - textBounds.MidX; + var y = bounds.MidY - textBounds.MidY; + canvas.DrawText(text, x, y, textPaint); + } - protected virtual void OnItemTapped(int index, object item) - { - SelectedIndex = index; - this.ItemTapped?.Invoke(this, new ItemsViewItemTappedEventArgs(index, item)); - Invalidate(); - } + private void DrawScrollBar(SKCanvas canvas, SKRect bounds) + { + var trackRect = new SKRect( + bounds.Right - _scrollBarWidth, + bounds.Top, + bounds.Right, + bounds.Bottom); - public override void OnScroll(ScrollEventArgs e) - { - float num = e.DeltaY * 20f; - SetScrollOffset(_scrollOffset + num); - e.Handled = true; - } + // Draw track + using var trackPaint = new SKPaint + { + Color = _scrollBarTrackColor, + Style = SKPaintStyle.Fill + }; + canvas.DrawRect(trackRect, trackPaint); - private void SetScrollOffset(float offset) - { - float scrollOffset = _scrollOffset; - _scrollOffset = Math.Clamp(offset, 0f, MaxScrollOffset); - if (Math.Abs(_scrollOffset - scrollOffset) > 0.1f) - { - this.Scrolled?.Invoke(this, new ItemsScrolledEventArgs(_scrollOffset, TotalContentHeight)); - Invalidate(); - } - } + // Calculate thumb size and position + var viewportRatio = bounds.Height / TotalContentHeight; + var thumbHeight = Math.Max(20, bounds.Height * viewportRatio); + var scrollRatio = _scrollOffset / MaxScrollOffset; + var thumbY = bounds.Top + (bounds.Height - thumbHeight) * scrollRatio; - public void ScrollToIndex(int index, bool animate = true) - { - if (index >= 0 && index < _items.Count) - { - float itemOffset = GetItemOffset(index); - SetScrollOffset(itemOffset); - } - } + var thumbRect = new SKRect( + bounds.Right - _scrollBarWidth + 1, + thumbY, + bounds.Right - 1, + thumbY + thumbHeight); - public void ScrollToItem(object item, bool animate = true) - { - int num = _items.IndexOf(item); - if (num >= 0) - { - ScrollToIndex(num, animate); - } - } + // Draw thumb + using var thumbPaint = new SKPaint + { + Color = _scrollBarColor, + Style = SKPaintStyle.Fill, + IsAntialias = true + }; - public override void OnKeyDown(KeyEventArgs e) - { - //IL_00b8: Unknown result type (might be due to invalid IL or missing references) - //IL_00bd: Unknown result type (might be due to invalid IL or missing references) - //IL_00db: Unknown result type (might be due to invalid IL or missing references) - //IL_00e0: Unknown result type (might be due to invalid IL or missing references) - if (!base.IsEnabled) - { - return; - } - SKRect bounds; - switch (e.Key) - { - case Key.Up: - if (SelectedIndex > 0) - { - SelectedIndex--; - EnsureIndexVisible(SelectedIndex); - Invalidate(); - } - e.Handled = true; - break; - case Key.Down: - if (SelectedIndex < _items.Count - 1) - { - SelectedIndex++; - EnsureIndexVisible(SelectedIndex); - Invalidate(); - } - e.Handled = true; - break; - case Key.PageUp: - { - float scrollOffset = _scrollOffset; - bounds = base.Bounds; - SetScrollOffset(scrollOffset - ((SKRect)(ref bounds)).Height); - e.Handled = true; - break; - } - case Key.PageDown: - { - float scrollOffset2 = _scrollOffset; - bounds = base.Bounds; - SetScrollOffset(scrollOffset2 + ((SKRect)(ref bounds)).Height); - e.Handled = true; - break; - } - case Key.Home: - SelectedIndex = 0; - SetScrollOffset(0f); - Invalidate(); - e.Handled = true; - break; - case Key.End: - SelectedIndex = _items.Count - 1; - SetScrollOffset(MaxScrollOffset); - Invalidate(); - e.Handled = true; - break; - case Key.Enter: - if (SelectedIndex >= 0 && SelectedIndex < _items.Count) - { - OnItemTapped(SelectedIndex, _items[SelectedIndex]); - } - e.Handled = true; - break; - } - } + var cornerRadius = (_scrollBarWidth - 2) / 2; + canvas.DrawRoundRect(new SKRoundRect(thumbRect, cornerRadius), thumbPaint); + } - private void EnsureIndexVisible(int index) - { - //IL_002b: Unknown result type (might be due to invalid IL or missing references) - //IL_0030: Unknown result type (might be due to invalid IL or missing references) - //IL_003e: Unknown result type (might be due to invalid IL or missing references) - //IL_0043: Unknown result type (might be due to invalid IL or missing references) - float itemOffset = GetItemOffset(index); - float num = itemOffset + GetItemHeight(index); - if (itemOffset < _scrollOffset) - { - SetScrollOffset(itemOffset); - return; - } - float scrollOffset = _scrollOffset; - SKRect bounds = base.Bounds; - if (num > scrollOffset + ((SKRect)(ref bounds)).Height) - { - bounds = base.Bounds; - SetScrollOffset(num - ((SKRect)(ref bounds)).Height); - } - } + public override void OnPointerPressed(PointerEventArgs e) + { + Console.WriteLine($"[SkiaItemsView] OnPointerPressed - x={e.X}, y={e.Y}, Bounds={Bounds}, ScreenBounds={ScreenBounds}, ItemCount={_items.Count}"); + if (!IsEnabled) return; - protected object? GetItemAt(int index) - { - if (index < 0 || index >= _items.Count) - { - return null; - } - return _items[index]; - } + // Check if clicking on scrollbar thumb + if (_showVerticalScrollBar && TotalContentHeight > Bounds.Height) + { + var thumbBounds = GetScrollbarThumbBounds(); + if (thumbBounds.Contains(e.X, e.Y)) + { + _isDraggingScrollbar = true; + _scrollbarDragStartY = e.Y; + _scrollbarDragStartScrollOffset = _scrollOffset; + // Cache values to prevent stutter + var thumbHeight = Math.Max(20, Bounds.Height * (Bounds.Height / TotalContentHeight)); + _scrollbarDragAvailableTrack = Bounds.Height - thumbHeight; + _scrollbarDragMaxScroll = MaxScrollOffset; + return; + } + } - public SkiaView? GetItemView(int index) - { - if (!_itemViewCache.TryGetValue(index, out SkiaView value)) - { - return null; - } - return value; - } + // Regular content drag + _isDragging = true; + _dragStartY = e.Y; + _dragStartOffset = _scrollOffset; + _lastDragTime = DateTime.Now; + _velocity = 0; + } - public override SkiaView? HitTest(float x, float y) - { - //IL_0009: Unknown result type (might be due to invalid IL or missing references) - //IL_000e: Unknown result type (might be due to invalid IL or missing references) - //IL_0013: Unknown result type (might be due to invalid IL or missing references) - //IL_0030: Unknown result type (might be due to invalid IL or missing references) - //IL_0035: Unknown result type (might be due to invalid IL or missing references) - //IL_0042: Unknown result type (might be due to invalid IL or missing references) - //IL_0047: Unknown result type (might be due to invalid IL or missing references) - //IL_0057: Unknown result type (might be due to invalid IL or missing references) - //IL_005c: Unknown result type (might be due to invalid IL or missing references) - //IL_0065: Unknown result type (might be due to invalid IL or missing references) - //IL_006a: Unknown result type (might be due to invalid IL or missing references) - //IL_0073: Unknown result type (might be due to invalid IL or missing references) - //IL_0078: Unknown result type (might be due to invalid IL or missing references) - if (base.IsVisible) - { - SKRect bounds = base.Bounds; - if (((SKRect)(ref bounds)).Contains(new SKPoint(x, y))) - { - if (_showVerticalScrollBar) - { - float totalContentHeight = TotalContentHeight; - bounds = base.Bounds; - if (totalContentHeight > ((SKRect)(ref bounds)).Height) - { - bounds = base.Bounds; - float num = ((SKRect)(ref bounds)).Right - _scrollBarWidth; - bounds = base.Bounds; - float top = ((SKRect)(ref bounds)).Top; - bounds = base.Bounds; - float right = ((SKRect)(ref bounds)).Right; - bounds = base.Bounds; - SKRect val = default(SKRect); - ((SKRect)(ref val))._002Ector(num, top, right, ((SKRect)(ref bounds)).Bottom); - ((SKRect)(ref val)).Contains(x, y); - return this; - } - } - return this; - } - } - return null; - } + /// + /// Gets the bounds of the scrollbar thumb in screen coordinates. + /// + private SKRect GetScrollbarThumbBounds() + { + // Use ScreenBounds for hit testing (input events use screen coordinates) + var screenBounds = ScreenBounds; + var viewportRatio = screenBounds.Height / TotalContentHeight; + var thumbHeight = Math.Max(20, screenBounds.Height * viewportRatio); + var scrollRatio = MaxScrollOffset > 0 ? _scrollOffset / MaxScrollOffset : 0; + var thumbY = screenBounds.Top + (screenBounds.Height - thumbHeight) * scrollRatio; - protected override SKSize MeasureOverride(SKSize availableSize) - { - //IL_006d: Unknown result type (might be due to invalid IL or missing references) - float num = ((((SKSize)(ref availableSize)).Width < float.MaxValue) ? ((SKSize)(ref availableSize)).Width : 200f); - float num2 = ((((SKSize)(ref availableSize)).Height < float.MaxValue) ? ((SKSize)(ref availableSize)).Height : 300f); - if (Math.Abs(num - _lastMeasuredWidth) > 5f) - { - _itemHeights.Clear(); - _itemViewCache.Clear(); - _lastMeasuredWidth = num; - } - return new SKSize(num, num2); - } + return new SKRect( + screenBounds.Right - _scrollBarWidth, + thumbY, + screenBounds.Right, + thumbY + thumbHeight); + } - protected override void Dispose(bool disposing) - { - if (disposing && _itemsSource is INotifyCollectionChanged notifyCollectionChanged) - { - notifyCollectionChanged.CollectionChanged -= OnCollectionChanged; - } - base.Dispose(disposing); - } + public override void OnPointerMoved(PointerEventArgs e) + { + // Handle scrollbar dragging - use cached values to prevent stutter + if (_isDraggingScrollbar) + { + if (_scrollbarDragAvailableTrack > 0) + { + var deltaY = e.Y - _scrollbarDragStartY; + var scrollDelta = (deltaY / _scrollbarDragAvailableTrack) * _scrollbarDragMaxScroll; + SetScrollOffset(_scrollbarDragStartScrollOffset + scrollDelta); + } + return; + } + + if (!_isDragging) return; + + var delta = _dragStartY - e.Y; + var newOffset = _dragStartOffset + delta; + + // Calculate velocity for momentum scrolling + var now = DateTime.Now; + var timeDelta = (now - _lastDragTime).TotalSeconds; + if (timeDelta > 0) + { + _velocity = (float)((_scrollOffset - newOffset) / timeDelta); + } + _lastDragTime = now; + + SetScrollOffset(newOffset); + } + + public override void OnPointerReleased(PointerEventArgs e) + { + // Handle scrollbar drag release + if (_isDraggingScrollbar) + { + _isDraggingScrollbar = false; + return; + } + + if (_isDragging) + { + _isDragging = false; + + // Check for tap (minimal movement) + var totalDrag = Math.Abs(e.Y - _dragStartY); + if (totalDrag < 5) + { + // This was a tap - find which item was tapped using variable heights + var screenBounds = ScreenBounds; + var localY = e.Y - screenBounds.Top + _scrollOffset; + + // Find tapped index by walking through item heights + int tappedIndex = -1; + float cumulativeY = 0; + for (int i = 0; i < _items.Count; i++) + { + var itemH = GetItemHeight(i); + if (localY >= cumulativeY && localY < cumulativeY + itemH) + { + tappedIndex = i; + break; + } + cumulativeY += itemH + _itemSpacing; + } + + Console.WriteLine($"[SkiaItemsView] Tap at Y={e.Y}, screenBounds.Top={screenBounds.Top}, scrollOffset={_scrollOffset}, localY={localY}, index={tappedIndex}"); + + if (tappedIndex >= 0 && tappedIndex < _items.Count) + { + OnItemTapped(tappedIndex, _items[tappedIndex]); + } + } + } + } + + /// + /// Gets the total Y scroll offset from all parent ScrollViews. + /// + private float GetTotalParentScrollY() + { + float total = 0; + var parent = Parent; + while (parent != null) + { + if (parent is SkiaScrollView scrollView) + { + total += scrollView.ScrollY; + } + parent = parent.Parent; + } + return total; + } + + protected virtual void OnItemTapped(int index, object item) + { + SelectedIndex = index; + ItemTapped?.Invoke(this, new ItemsViewItemTappedEventArgs(index, item)); + Invalidate(); + } + + public override void OnScroll(ScrollEventArgs e) + { + var delta = e.DeltaY * 20; + SetScrollOffset(_scrollOffset + delta); + e.Handled = true; + } + + private void SetScrollOffset(float offset) + { + var oldOffset = _scrollOffset; + _scrollOffset = Math.Clamp(offset, 0, MaxScrollOffset); + + if (Math.Abs(_scrollOffset - oldOffset) > 0.1f) + { + Scrolled?.Invoke(this, new ItemsScrolledEventArgs(_scrollOffset, TotalContentHeight)); + Invalidate(); + } + } + + public void ScrollToIndex(int index, bool animate = true) + { + if (index < 0 || index >= _items.Count) return; + + var targetOffset = GetItemOffset(index); + SetScrollOffset(targetOffset); + } + + public void ScrollToItem(object item, bool animate = true) + { + var index = _items.IndexOf(item); + if (index >= 0) + { + ScrollToIndex(index, animate); + } + } + + public override void OnKeyDown(KeyEventArgs e) + { + if (!IsEnabled) return; + + switch (e.Key) + { + case Key.Up: + if (SelectedIndex > 0) + { + SelectedIndex--; + EnsureIndexVisible(SelectedIndex); + Invalidate(); + } + e.Handled = true; + break; + + case Key.Down: + if (SelectedIndex < _items.Count - 1) + { + SelectedIndex++; + EnsureIndexVisible(SelectedIndex); + Invalidate(); + } + e.Handled = true; + break; + + case Key.PageUp: + SetScrollOffset(_scrollOffset - Bounds.Height); + e.Handled = true; + break; + + case Key.PageDown: + SetScrollOffset(_scrollOffset + Bounds.Height); + e.Handled = true; + break; + + case Key.Home: + SelectedIndex = 0; + SetScrollOffset(0); + Invalidate(); + e.Handled = true; + break; + + case Key.End: + SelectedIndex = _items.Count - 1; + SetScrollOffset(MaxScrollOffset); + Invalidate(); + e.Handled = true; + break; + + case Key.Enter: + if (SelectedIndex >= 0 && SelectedIndex < _items.Count) + { + OnItemTapped(SelectedIndex, _items[SelectedIndex]); + } + e.Handled = true; + break; + } + } + + private void EnsureIndexVisible(int index) + { + var itemTop = GetItemOffset(index); + var itemBottom = itemTop + GetItemHeight(index); + + if (itemTop < _scrollOffset) + { + SetScrollOffset(itemTop); + } + else if (itemBottom > _scrollOffset + Bounds.Height) + { + SetScrollOffset(itemBottom - Bounds.Height); + } + } + + protected int ItemCount => _items.Count; + protected object? GetItemAt(int index) => index >= 0 && index < _items.Count ? _items[index] : null; + + /// + /// Override HitTest to handle scrollbar clicks properly. + /// HitTest receives content-space coordinates (already transformed by parent ScrollView). + /// + public override SkiaView? HitTest(float x, float y) + { + // HitTest uses Bounds (content space) - coordinates are transformed by parent + if (!IsVisible || !Bounds.Contains(new SKPoint(x, y))) + return null; + + // Check scrollbar area FIRST before content + // This ensures scrollbar clicks are handled by this view + if (_showVerticalScrollBar && TotalContentHeight > Bounds.Height) + { + var trackArea = new SKRect(Bounds.Right - _scrollBarWidth, Bounds.Top, Bounds.Right, Bounds.Bottom); + if (trackArea.Contains(x, y)) + return this; + } + + return this; + } + + protected override SKSize MeasureOverride(SKSize availableSize) + { + var width = availableSize.Width < float.MaxValue ? availableSize.Width : 200; + var height = availableSize.Height < float.MaxValue ? availableSize.Height : 300; + + // Clear item caches when width changes significantly (items need re-measurement for text wrapping) + if (Math.Abs(width - _lastMeasuredWidth) > 5) + { + _itemHeights.Clear(); + _itemViewCache.Clear(); + _lastMeasuredWidth = width; + } + + // Items view takes all available space + return new SKSize(width, height); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + if (_itemsSource is INotifyCollectionChanged collection) + { + collection.CollectionChanged -= OnCollectionChanged; + } + } + base.Dispose(disposing); + } +} + +/// +/// Event args for items view scroll events. +/// +public class ItemsScrolledEventArgs : EventArgs +{ + public float ScrollOffset { get; } + public float TotalHeight { get; } + + public ItemsScrolledEventArgs(float scrollOffset, float totalHeight) + { + ScrollOffset = scrollOffset; + TotalHeight = totalHeight; + } +} + +/// +/// Event args for items view item tap events. +/// +public class ItemsViewItemTappedEventArgs : EventArgs +{ + public int Index { get; } + public object Item { get; } + + public ItemsViewItemTappedEventArgs(int index, object item) + { + Index = index; + Item = item; + } } diff --git a/Views/SkiaLabel.cs b/Views/SkiaLabel.cs index d60a212..4d2ca5c 100644 --- a/Views/SkiaLabel.cs +++ b/Views/SkiaLabel.cs @@ -1,1081 +1,861 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Microsoft.Maui.Controls; -using Microsoft.Maui.Platform.Linux.Rendering; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using SkiaSharp; +using Microsoft.Maui.Platform.Linux.Rendering; namespace Microsoft.Maui.Platform; +/// +/// Skia-rendered label control for displaying text with full XAML styling support. +/// public class SkiaLabel : SkiaView { - public static readonly BindableProperty TextProperty = BindableProperty.Create("Text", typeof(string), typeof(SkiaLabel), (object)"", (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaLabel)(object)b).OnTextChanged(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + #region BindableProperties - public static readonly BindableProperty FormattedSpansProperty = BindableProperty.Create("FormattedSpans", typeof(IList), typeof(SkiaLabel), (object)null, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaLabel)(object)b).OnTextChanged(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for Text. + /// + public static readonly BindableProperty TextProperty = + BindableProperty.Create( + nameof(Text), + typeof(string), + typeof(SkiaLabel), + "", + propertyChanged: (b, o, n) => ((SkiaLabel)b).OnTextChanged()); - public static readonly BindableProperty TextColorProperty = BindableProperty.Create("TextColor", typeof(SKColor), typeof(SkiaLabel), (object)SKColors.Black, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaLabel)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for TextColor. + /// + public static readonly BindableProperty TextColorProperty = + BindableProperty.Create( + nameof(TextColor), + typeof(SKColor), + typeof(SkiaLabel), + SKColors.Black, + propertyChanged: (b, o, n) => ((SkiaLabel)b).Invalidate()); - public static readonly BindableProperty FontFamilyProperty = BindableProperty.Create("FontFamily", typeof(string), typeof(SkiaLabel), (object)"Sans", (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaLabel)(object)b).OnFontChanged(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for FontFamily. + /// + public static readonly BindableProperty FontFamilyProperty = + BindableProperty.Create( + nameof(FontFamily), + typeof(string), + typeof(SkiaLabel), + "Sans", + propertyChanged: (b, o, n) => ((SkiaLabel)b).OnFontChanged()); - public static readonly BindableProperty FontSizeProperty = BindableProperty.Create("FontSize", typeof(float), typeof(SkiaLabel), (object)14f, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaLabel)(object)b).OnFontChanged(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for FontSize. + /// + public static readonly BindableProperty FontSizeProperty = + BindableProperty.Create( + nameof(FontSize), + typeof(float), + typeof(SkiaLabel), + 14f, + propertyChanged: (b, o, n) => ((SkiaLabel)b).OnFontChanged()); - public static readonly BindableProperty IsBoldProperty = BindableProperty.Create("IsBold", typeof(bool), typeof(SkiaLabel), (object)false, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaLabel)(object)b).OnFontChanged(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for IsBold. + /// + public static readonly BindableProperty IsBoldProperty = + BindableProperty.Create( + nameof(IsBold), + typeof(bool), + typeof(SkiaLabel), + false, + propertyChanged: (b, o, n) => ((SkiaLabel)b).OnFontChanged()); - public static readonly BindableProperty IsItalicProperty = BindableProperty.Create("IsItalic", typeof(bool), typeof(SkiaLabel), (object)false, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaLabel)(object)b).OnFontChanged(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for IsItalic. + /// + public static readonly BindableProperty IsItalicProperty = + BindableProperty.Create( + nameof(IsItalic), + typeof(bool), + typeof(SkiaLabel), + false, + propertyChanged: (b, o, n) => ((SkiaLabel)b).OnFontChanged()); - public static readonly BindableProperty IsUnderlineProperty = BindableProperty.Create("IsUnderline", typeof(bool), typeof(SkiaLabel), (object)false, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaLabel)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for IsUnderline. + /// + public static readonly BindableProperty IsUnderlineProperty = + BindableProperty.Create( + nameof(IsUnderline), + typeof(bool), + typeof(SkiaLabel), + false, + propertyChanged: (b, o, n) => ((SkiaLabel)b).Invalidate()); - public static readonly BindableProperty IsStrikethroughProperty = BindableProperty.Create("IsStrikethrough", typeof(bool), typeof(SkiaLabel), (object)false, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaLabel)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for IsStrikethrough. + /// + public static readonly BindableProperty IsStrikethroughProperty = + BindableProperty.Create( + nameof(IsStrikethrough), + typeof(bool), + typeof(SkiaLabel), + false, + propertyChanged: (b, o, n) => ((SkiaLabel)b).Invalidate()); - public static readonly BindableProperty HorizontalTextAlignmentProperty = BindableProperty.Create("HorizontalTextAlignment", typeof(TextAlignment), typeof(SkiaLabel), (object)TextAlignment.Start, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaLabel)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for HorizontalTextAlignment. + /// + public static readonly BindableProperty HorizontalTextAlignmentProperty = + BindableProperty.Create( + nameof(HorizontalTextAlignment), + typeof(TextAlignment), + typeof(SkiaLabel), + TextAlignment.Start, + propertyChanged: (b, o, n) => ((SkiaLabel)b).Invalidate()); - public static readonly BindableProperty VerticalTextAlignmentProperty = BindableProperty.Create("VerticalTextAlignment", typeof(TextAlignment), typeof(SkiaLabel), (object)TextAlignment.Center, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaLabel)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for VerticalTextAlignment. + /// + public static readonly BindableProperty VerticalTextAlignmentProperty = + BindableProperty.Create( + nameof(VerticalTextAlignment), + typeof(TextAlignment), + typeof(SkiaLabel), + TextAlignment.Center, + propertyChanged: (b, o, n) => ((SkiaLabel)b).Invalidate()); - public static readonly BindableProperty LineBreakModeProperty = BindableProperty.Create("LineBreakMode", typeof(LineBreakMode), typeof(SkiaLabel), (object)LineBreakMode.TailTruncation, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaLabel)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for LineBreakMode. + /// + public static readonly BindableProperty LineBreakModeProperty = + BindableProperty.Create( + nameof(LineBreakMode), + typeof(LineBreakMode), + typeof(SkiaLabel), + LineBreakMode.TailTruncation, + propertyChanged: (b, o, n) => ((SkiaLabel)b).Invalidate()); - public static readonly BindableProperty MaxLinesProperty = BindableProperty.Create("MaxLines", typeof(int), typeof(SkiaLabel), (object)0, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaLabel)(object)b).OnTextChanged(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for MaxLines. + /// + public static readonly BindableProperty MaxLinesProperty = + BindableProperty.Create( + nameof(MaxLines), + typeof(int), + typeof(SkiaLabel), + 0, + propertyChanged: (b, o, n) => ((SkiaLabel)b).OnTextChanged()); - public static readonly BindableProperty LineHeightProperty = BindableProperty.Create("LineHeight", typeof(float), typeof(SkiaLabel), (object)1.2f, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaLabel)(object)b).OnTextChanged(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for LineHeight. + /// + public static readonly BindableProperty LineHeightProperty = + BindableProperty.Create( + nameof(LineHeight), + typeof(float), + typeof(SkiaLabel), + 1.2f, + propertyChanged: (b, o, n) => ((SkiaLabel)b).OnTextChanged()); - public static readonly BindableProperty CharacterSpacingProperty = BindableProperty.Create("CharacterSpacing", typeof(float), typeof(SkiaLabel), (object)0f, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaLabel)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for CharacterSpacing. + /// + public static readonly BindableProperty CharacterSpacingProperty = + BindableProperty.Create( + nameof(CharacterSpacing), + typeof(float), + typeof(SkiaLabel), + 0f, + propertyChanged: (b, o, n) => ((SkiaLabel)b).Invalidate()); - public static readonly BindableProperty PaddingProperty = BindableProperty.Create("Padding", typeof(SKRect), typeof(SkiaLabel), (object)SKRect.Empty, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaLabel)(object)b).OnTextChanged(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for Padding. + /// + public static readonly BindableProperty PaddingProperty = + BindableProperty.Create( + nameof(Padding), + typeof(SKRect), + typeof(SkiaLabel), + SKRect.Empty, + propertyChanged: (b, o, n) => ((SkiaLabel)b).OnTextChanged()); - private static SKTypeface? _cachedTypeface; + #endregion - public string Text - { - get - { - return (string)((BindableObject)this).GetValue(TextProperty); - } - set - { - ((BindableObject)this).SetValue(TextProperty, (object)value); - } - } + #region Properties - public IList? FormattedSpans - { - get - { - return (IList)((BindableObject)this).GetValue(FormattedSpansProperty); - } - set - { - ((BindableObject)this).SetValue(FormattedSpansProperty, (object)value); - } - } + /// + /// Gets or sets the text content. + /// + public string Text + { + get => (string)GetValue(TextProperty); + set => SetValue(TextProperty, value); + } - public SKColor TextColor - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKColor)((BindableObject)this).GetValue(TextColorProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(TextColorProperty, (object)value); - } - } + /// + /// Gets or sets the text color. + /// + public SKColor TextColor + { + get => (SKColor)GetValue(TextColorProperty); + set => SetValue(TextColorProperty, value); + } - public string FontFamily - { - get - { - return (string)((BindableObject)this).GetValue(FontFamilyProperty); - } - set - { - ((BindableObject)this).SetValue(FontFamilyProperty, (object)value); - } - } + /// + /// Gets or sets the font family. + /// + public string FontFamily + { + get => (string)GetValue(FontFamilyProperty); + set => SetValue(FontFamilyProperty, value); + } - public float FontSize - { - get - { - return (float)((BindableObject)this).GetValue(FontSizeProperty); - } - set - { - ((BindableObject)this).SetValue(FontSizeProperty, (object)value); - } - } + /// + /// Gets or sets the font size. + /// + public float FontSize + { + get => (float)GetValue(FontSizeProperty); + set => SetValue(FontSizeProperty, value); + } - public bool IsBold - { - get - { - return (bool)((BindableObject)this).GetValue(IsBoldProperty); - } - set - { - ((BindableObject)this).SetValue(IsBoldProperty, (object)value); - } - } + /// + /// Gets or sets whether the text is bold. + /// + public bool IsBold + { + get => (bool)GetValue(IsBoldProperty); + set => SetValue(IsBoldProperty, value); + } - public bool IsItalic - { - get - { - return (bool)((BindableObject)this).GetValue(IsItalicProperty); - } - set - { - ((BindableObject)this).SetValue(IsItalicProperty, (object)value); - } - } + /// + /// Gets or sets whether the text is italic. + /// + public bool IsItalic + { + get => (bool)GetValue(IsItalicProperty); + set => SetValue(IsItalicProperty, value); + } - public bool IsUnderline - { - get - { - return (bool)((BindableObject)this).GetValue(IsUnderlineProperty); - } - set - { - ((BindableObject)this).SetValue(IsUnderlineProperty, (object)value); - } - } + /// + /// Gets or sets whether the text has underline. + /// + public bool IsUnderline + { + get => (bool)GetValue(IsUnderlineProperty); + set => SetValue(IsUnderlineProperty, value); + } - public bool IsStrikethrough - { - get - { - return (bool)((BindableObject)this).GetValue(IsStrikethroughProperty); - } - set - { - ((BindableObject)this).SetValue(IsStrikethroughProperty, (object)value); - } - } + /// + /// Gets or sets whether the text has strikethrough. + /// + public bool IsStrikethrough + { + get => (bool)GetValue(IsStrikethroughProperty); + set => SetValue(IsStrikethroughProperty, value); + } - public TextAlignment HorizontalTextAlignment - { - get - { - return (TextAlignment)((BindableObject)this).GetValue(HorizontalTextAlignmentProperty); - } - set - { - ((BindableObject)this).SetValue(HorizontalTextAlignmentProperty, (object)value); - } - } + /// + /// Gets or sets the horizontal text alignment. + /// + public TextAlignment HorizontalTextAlignment + { + get => (TextAlignment)GetValue(HorizontalTextAlignmentProperty); + set => SetValue(HorizontalTextAlignmentProperty, value); + } - public TextAlignment VerticalTextAlignment - { - get - { - return (TextAlignment)((BindableObject)this).GetValue(VerticalTextAlignmentProperty); - } - set - { - ((BindableObject)this).SetValue(VerticalTextAlignmentProperty, (object)value); - } - } + /// + /// Gets or sets the vertical text alignment. + /// + public TextAlignment VerticalTextAlignment + { + get => (TextAlignment)GetValue(VerticalTextAlignmentProperty); + set => SetValue(VerticalTextAlignmentProperty, value); + } - public LineBreakMode LineBreakMode - { - get - { - return (LineBreakMode)((BindableObject)this).GetValue(LineBreakModeProperty); - } - set - { - ((BindableObject)this).SetValue(LineBreakModeProperty, (object)value); - } - } + /// + /// Gets or sets the line break mode. + /// + public LineBreakMode LineBreakMode + { + get => (LineBreakMode)GetValue(LineBreakModeProperty); + set => SetValue(LineBreakModeProperty, value); + } - public int MaxLines - { - get - { - return (int)((BindableObject)this).GetValue(MaxLinesProperty); - } - set - { - ((BindableObject)this).SetValue(MaxLinesProperty, (object)value); - } - } + /// + /// Gets or sets the maximum number of lines. 0 = unlimited. + /// + public int MaxLines + { + get => (int)GetValue(MaxLinesProperty); + set => SetValue(MaxLinesProperty, value); + } - public float LineHeight - { - get - { - return (float)((BindableObject)this).GetValue(LineHeightProperty); - } - set - { - ((BindableObject)this).SetValue(LineHeightProperty, (object)value); - } - } + /// + /// Gets or sets the line height multiplier. + /// + public float LineHeight + { + get => (float)GetValue(LineHeightProperty); + set => SetValue(LineHeightProperty, value); + } - public float CharacterSpacing - { - get - { - return (float)((BindableObject)this).GetValue(CharacterSpacingProperty); - } - set - { - ((BindableObject)this).SetValue(CharacterSpacingProperty, (object)value); - } - } + /// + /// Gets or sets the character spacing. + /// + public float CharacterSpacing + { + get => (float)GetValue(CharacterSpacingProperty); + set => SetValue(CharacterSpacingProperty, value); + } - public SKRect Padding - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKRect)((BindableObject)this).GetValue(PaddingProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(PaddingProperty, (object)value); - } - } + /// + /// Gets or sets the padding. + /// + public SKRect Padding + { + get => (SKRect)GetValue(PaddingProperty); + set => SetValue(PaddingProperty, value); + } - public SkiaTextAlignment HorizontalAlignment - { - get - { - return HorizontalTextAlignment switch - { - TextAlignment.Start => SkiaTextAlignment.Left, - TextAlignment.Center => SkiaTextAlignment.Center, - TextAlignment.End => SkiaTextAlignment.Right, - _ => SkiaTextAlignment.Left, - }; - } - set - { - HorizontalTextAlignment = value switch - { - SkiaTextAlignment.Left => TextAlignment.Start, - SkiaTextAlignment.Center => TextAlignment.Center, - SkiaTextAlignment.Right => TextAlignment.End, - _ => TextAlignment.Start, - }; - } - } + /// + /// Gets or sets the horizontal alignment (compatibility property). + /// + public SkiaTextAlignment HorizontalAlignment + { + get => HorizontalTextAlignment switch + { + TextAlignment.Start => SkiaTextAlignment.Left, + TextAlignment.Center => SkiaTextAlignment.Center, + TextAlignment.End => SkiaTextAlignment.Right, + _ => SkiaTextAlignment.Left + }; + set => HorizontalTextAlignment = value switch + { + SkiaTextAlignment.Left => TextAlignment.Start, + SkiaTextAlignment.Center => TextAlignment.Center, + SkiaTextAlignment.Right => TextAlignment.End, + _ => TextAlignment.Start + }; + } - public SkiaVerticalAlignment VerticalAlignment - { - get - { - return VerticalTextAlignment switch - { - TextAlignment.Start => SkiaVerticalAlignment.Top, - TextAlignment.Center => SkiaVerticalAlignment.Center, - TextAlignment.End => SkiaVerticalAlignment.Bottom, - _ => SkiaVerticalAlignment.Top, - }; - } - set - { - VerticalTextAlignment = value switch - { - SkiaVerticalAlignment.Top => TextAlignment.Start, - SkiaVerticalAlignment.Center => TextAlignment.Center, - SkiaVerticalAlignment.Bottom => TextAlignment.End, - _ => TextAlignment.Start, - }; - } - } + /// + /// Gets or sets the vertical alignment (compatibility property). + /// + public SkiaVerticalAlignment VerticalAlignment + { + get => VerticalTextAlignment switch + { + TextAlignment.Start => SkiaVerticalAlignment.Top, + TextAlignment.Center => SkiaVerticalAlignment.Center, + TextAlignment.End => SkiaVerticalAlignment.Bottom, + _ => SkiaVerticalAlignment.Top + }; + set => VerticalTextAlignment = value switch + { + SkiaVerticalAlignment.Top => TextAlignment.Start, + SkiaVerticalAlignment.Center => TextAlignment.Center, + SkiaVerticalAlignment.Bottom => TextAlignment.End, + _ => TextAlignment.Start + }; + } - public event EventHandler? Tapped; + #endregion - private void OnTextChanged() - { - InvalidateMeasure(); - Invalidate(); - } + private static SKTypeface? _cachedTypeface; - private void OnFontChanged() - { - InvalidateMeasure(); - Invalidate(); - } + private void OnTextChanged() + { + InvalidateMeasure(); + Invalidate(); + } - private static SKTypeface GetLinuxTypeface() - { - if (_cachedTypeface != null) - { - return _cachedTypeface; - } - string[] array = new string[4] { "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", "/usr/share/fonts/truetype/ubuntu/Ubuntu-R.ttf", "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf", "/usr/share/fonts/truetype/noto/NotoSans-Regular.ttf" }; - foreach (string text in array) - { - if (File.Exists(text)) - { - _cachedTypeface = SKTypeface.FromFile(text, 0); - if (_cachedTypeface != null) - { - return _cachedTypeface; - } - } - } - return SKTypeface.Default; - } + private void OnFontChanged() + { + InvalidateMeasure(); + Invalidate(); + } - protected override void OnDraw(SKCanvas canvas, SKRect bounds) - { - //IL_000a: Unknown result type (might be due to invalid IL or missing references) - //IL_000f: Unknown result type (might be due to invalid IL or missing references) - //IL_0021: Unknown result type (might be due to invalid IL or missing references) - //IL_0026: Unknown result type (might be due to invalid IL or missing references) - //IL_0038: Unknown result type (might be due to invalid IL or missing references) - //IL_003d: Unknown result type (might be due to invalid IL or missing references) - //IL_004f: Unknown result type (might be due to invalid IL or missing references) - //IL_0054: Unknown result type (might be due to invalid IL or missing references) - //IL_007b: Unknown result type (might be due to invalid IL or missing references) - //IL_00b1: Unknown result type (might be due to invalid IL or missing references) - //IL_00b7: Expected O, but got Unknown - //IL_00f7: Unknown result type (might be due to invalid IL or missing references) - //IL_00fd: Expected O, but got Unknown - //IL_00fe: Unknown result type (might be due to invalid IL or missing references) - //IL_0103: Unknown result type (might be due to invalid IL or missing references) - //IL_0123: Unknown result type (might be due to invalid IL or missing references) - //IL_010d: Unknown result type (might be due to invalid IL or missing references) - //IL_0112: Unknown result type (might be due to invalid IL or missing references) - //IL_011b: Unknown result type (might be due to invalid IL or missing references) - //IL_012d: Unknown result type (might be due to invalid IL or missing references) - //IL_0136: Expected O, but got Unknown - //IL_0177: Unknown result type (might be due to invalid IL or missing references) - //IL_016a: Unknown result type (might be due to invalid IL or missing references) - float left = ((SKRect)(ref bounds)).Left; - SKRect padding = Padding; - float num = left + ((SKRect)(ref padding)).Left; - float top = ((SKRect)(ref bounds)).Top; - padding = Padding; - float num2 = top + ((SKRect)(ref padding)).Top; - float right = ((SKRect)(ref bounds)).Right; - padding = Padding; - float num3 = right - ((SKRect)(ref padding)).Right; - float bottom = ((SKRect)(ref bounds)).Bottom; - padding = Padding; - SKRect bounds2 = default(SKRect); - ((SKRect)(ref bounds2))._002Ector(num, num2, num3, bottom - ((SKRect)(ref padding)).Bottom); - if (FormattedSpans != null && FormattedSpans.Count > 0) - { - DrawFormattedText(canvas, bounds2); - } - else - { - if (string.IsNullOrEmpty(Text)) - { - return; - } - SKFontStyle style = new SKFontStyle((SKFontStyleWeight)(IsBold ? 700 : 400), (SKFontStyleWidth)5, (SKFontStyleSlant)(IsItalic ? 1 : 0)); - SKTypeface val = SkiaRenderingEngine.Current?.ResourceCache.GetTypeface(FontFamily, style); - if (val == null || val == SKTypeface.Default) - { - val = GetLinuxTypeface(); - } - SKFont val2 = new SKFont(val, FontSize, 1f, 0f); - try - { - SKPaint val3 = new SKPaint(val2); - SKColor color; - if (!base.IsEnabled) - { - SKColor textColor = TextColor; - color = ((SKColor)(ref textColor)).WithAlpha((byte)128); - } - else - { - color = TextColor; - } - val3.Color = color; - val3.IsAntialias = true; - SKPaint val4 = val3; - try - { - if (MaxLines > 1 || Text.Contains('\n') || LineBreakMode == LineBreakMode.WordWrap || LineBreakMode == LineBreakMode.CharacterWrap) - { - DrawMultiLineWithWrapping(canvas, val4, val2, bounds2); - } - else - { - DrawSingleLine(canvas, val4, val2, bounds2); - } - } - finally - { - ((IDisposable)val4)?.Dispose(); - } - } - finally - { - ((IDisposable)val2)?.Dispose(); - } - } - } + private static SKTypeface GetLinuxTypeface() + { + if (_cachedTypeface != null) return _cachedTypeface; - private void DrawFormattedText(SKCanvas canvas, SKRect bounds) - { - //IL_0071: Unknown result type (might be due to invalid IL or missing references) - //IL_01d9: Unknown result type (might be due to invalid IL or missing references) - //IL_029b: Unknown result type (might be due to invalid IL or missing references) - //IL_021c: Unknown result type (might be due to invalid IL or missing references) - if (FormattedSpans == null || FormattedSpans.Count == 0) - { - return; - } - float num = ((SKRect)(ref bounds)).Left; - float num2 = ((SKRect)(ref bounds)).Top; - float num3 = 0f; - float val = 0f; - List<(SkiaTextSpan, float, float, float, SKPaint)> list = new List<(SkiaTextSpan, float, float, float, SKPaint)>(); - foreach (SkiaTextSpan formattedSpan in FormattedSpans) - { - if (!string.IsNullOrEmpty(formattedSpan.Text)) - { - SKPaint val2 = CreateSpanPaint(formattedSpan); - SKRect val3 = default(SKRect); - val2.MeasureText(formattedSpan.Text, ref val3); - num3 = Math.Max(num3, ((SKRect)(ref val3)).Height); - if (num + ((SKRect)(ref val3)).Width > ((SKRect)(ref bounds)).Right && num > ((SKRect)(ref bounds)).Left) - { - num2 += num3 * LineHeight; - num = ((SKRect)(ref bounds)).Left; - val = Math.Max(val, num); - num3 = ((SKRect)(ref val3)).Height; - } - list.Add((formattedSpan, num, ((SKRect)(ref val3)).Width, ((SKRect)(ref val3)).Height, val2)); - num += ((SKRect)(ref val3)).Width; - } - } - float num4 = num2 + num3 - ((SKRect)(ref bounds)).Top; - float num5 = VerticalTextAlignment switch - { - TextAlignment.Start => 0f, - TextAlignment.Center => (((SKRect)(ref bounds)).Height - num4) / 2f, - TextAlignment.End => ((SKRect)(ref bounds)).Height - num4, - _ => 0f, - }; - num = ((SKRect)(ref bounds)).Left; - num2 = ((SKRect)(ref bounds)).Top + num5; - num3 = 0f; - float left = ((SKRect)(ref bounds)).Left; - List<(SkiaTextSpan, float, float, float, SKPaint)> list2 = new List<(SkiaTextSpan, float, float, float, SKPaint)>(); - foreach (SkiaTextSpan formattedSpan2 in FormattedSpans) - { - if (!string.IsNullOrEmpty(formattedSpan2.Text)) - { - SKPaint val4 = CreateSpanPaint(formattedSpan2); - SKRect val5 = default(SKRect); - val4.MeasureText(formattedSpan2.Text, ref val5); - num3 = Math.Max(num3, ((SKRect)(ref val5)).Height); - if (num + ((SKRect)(ref val5)).Width > ((SKRect)(ref bounds)).Right && num > ((SKRect)(ref bounds)).Left) - { - DrawFormattedLine(canvas, bounds, list2, num2 + num3); - num2 += num3 * LineHeight; - num = ((SKRect)(ref bounds)).Left; - num3 = ((SKRect)(ref val5)).Height; - list2.Clear(); - } - list2.Add((formattedSpan2, num - left, ((SKRect)(ref val5)).Width, ((SKRect)(ref val5)).Height, val4)); - num += ((SKRect)(ref val5)).Width; - } - } - if (list2.Count > 0) - { - DrawFormattedLine(canvas, bounds, list2, num2 + num3); - } - } + // Try common Linux font paths + string[] fontPaths = { + "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", + "/usr/share/fonts/truetype/ubuntu/Ubuntu-R.ttf", + "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf", + "/usr/share/fonts/truetype/noto/NotoSans-Regular.ttf" + }; - private void DrawFormattedLine(SKCanvas canvas, SKRect bounds, List<(SkiaTextSpan span, float x, float width, float height, SKPaint paint)> lineSpans, float y) - { - //IL_00ef: Unknown result type (might be due to invalid IL or missing references) - //IL_00f4: Unknown result type (might be due to invalid IL or missing references) - //IL_0165: Unknown result type (might be due to invalid IL or missing references) - //IL_016a: Unknown result type (might be due to invalid IL or missing references) - //IL_016d: Unknown result type (might be due to invalid IL or missing references) - //IL_0177: Unknown result type (might be due to invalid IL or missing references) - //IL_0182: Unknown result type (might be due to invalid IL or missing references) - //IL_018b: Expected O, but got Unknown - //IL_0100: Unknown result type (might be due to invalid IL or missing references) - //IL_0105: Unknown result type (might be due to invalid IL or missing references) - //IL_0111: Unknown result type (might be due to invalid IL or missing references) - //IL_011b: Unknown result type (might be due to invalid IL or missing references) - //IL_0124: Expected O, but got Unknown - //IL_01bf: Unknown result type (might be due to invalid IL or missing references) - //IL_01c4: Unknown result type (might be due to invalid IL or missing references) - //IL_01c7: Unknown result type (might be due to invalid IL or missing references) - //IL_01d1: Unknown result type (might be due to invalid IL or missing references) - //IL_01dc: Unknown result type (might be due to invalid IL or missing references) - //IL_01e5: Expected O, but got Unknown - if (lineSpans.Count == 0) - { - return; - } - float num = 0f; - foreach (var lineSpan in lineSpans) - { - float item = lineSpan.width; - num += item; - } - float num2 = HorizontalTextAlignment switch - { - TextAlignment.Start => ((SKRect)(ref bounds)).Left, - TextAlignment.Center => ((SKRect)(ref bounds)).Left + (((SKRect)(ref bounds)).Width - num) / 2f, - TextAlignment.End => ((SKRect)(ref bounds)).Right - num, - _ => ((SKRect)(ref bounds)).Left, - }; - foreach (var (skiaTextSpan, _, num3, num4, val) in lineSpans) - { - if (skiaTextSpan.BackgroundColor.HasValue && skiaTextSpan.BackgroundColor.Value != SKColors.Transparent) - { - SKPaint val2 = new SKPaint - { - Color = skiaTextSpan.BackgroundColor.Value, - Style = (SKPaintStyle)0 - }; - try - { - canvas.DrawRect(num2, y - num4, num3, num4 + 4f, val2); - } - finally - { - ((IDisposable)val2)?.Dispose(); - } - } - canvas.DrawText(skiaTextSpan.Text, num2, y, val); - if (skiaTextSpan.IsUnderline) - { - SKPaint val3 = new SKPaint - { - Color = val.Color, - StrokeWidth = 1f, - IsAntialias = true - }; - try - { - canvas.DrawLine(num2, y + 2f, num2 + num3, y + 2f, val3); - } - finally - { - ((IDisposable)val3)?.Dispose(); - } - } - if (skiaTextSpan.IsStrikethrough) - { - SKPaint val4 = new SKPaint - { - Color = val.Color, - StrokeWidth = 1f, - IsAntialias = true - }; - try - { - canvas.DrawLine(num2, y - num4 / 3f, num2 + num3, y - num4 / 3f, val4); - } - finally - { - ((IDisposable)val4)?.Dispose(); - } - } - num2 += num3; - ((SKNativeObject)val).Dispose(); - } - } + foreach (var path in fontPaths) + { + if (System.IO.File.Exists(path)) + { + _cachedTypeface = SKTypeface.FromFile(path); + if (_cachedTypeface != null) return _cachedTypeface; + } + } + return SKTypeface.Default; + } - private SKPaint CreateSpanPaint(SkiaTextSpan span) - { - //IL_0021: Unknown result type (might be due to invalid IL or missing references) - //IL_0027: Expected O, but got Unknown - //IL_0088: Unknown result type (might be due to invalid IL or missing references) - //IL_008e: Expected O, but got Unknown - //IL_00a9: Unknown result type (might be due to invalid IL or missing references) - //IL_00a0: Unknown result type (might be due to invalid IL or missing references) - //IL_00ae: Unknown result type (might be due to invalid IL or missing references) - //IL_00c7: Unknown result type (might be due to invalid IL or missing references) - //IL_00cc: Unknown result type (might be due to invalid IL or missing references) - //IL_00cd: Unknown result type (might be due to invalid IL or missing references) - //IL_00d4: Unknown result type (might be due to invalid IL or missing references) - //IL_00dd: Expected O, but got Unknown - //IL_00bf: Unknown result type (might be due to invalid IL or missing references) - //IL_00c4: Unknown result type (might be due to invalid IL or missing references) - SKFontStyle style = new SKFontStyle((SKFontStyleWeight)(span.IsBold ? 700 : 400), (SKFontStyleWidth)5, (SKFontStyleSlant)(span.IsItalic ? 1 : 0)); - SKTypeface val = SkiaRenderingEngine.Current?.ResourceCache.GetTypeface(span.FontFamily ?? FontFamily, style); - if (val == null || val == SKTypeface.Default) - { - val = GetLinuxTypeface(); - } - float num = ((span.FontSize > 0f) ? span.FontSize : FontSize); - SKFont val2 = new SKFont(val, num, 1f, 0f); - try - { - SKColor color = (SKColor)(((_003F?)span.TextColor) ?? TextColor); - if (!base.IsEnabled) - { - color = ((SKColor)(ref color)).WithAlpha((byte)128); - } - return new SKPaint(val2) - { - Color = color, - IsAntialias = true - }; - } - finally - { - ((IDisposable)val2)?.Dispose(); - } - } + protected override void OnDraw(SKCanvas canvas, SKRect bounds) + { + if (string.IsNullOrEmpty(Text)) + return; - private void DrawSingleLine(SKCanvas canvas, SKPaint paint, SKFont font, SKRect bounds) - { - //IL_0009: Unknown result type (might be due to invalid IL or missing references) - //IL_0128: Unknown result type (might be due to invalid IL or missing references) - //IL_012d: Unknown result type (might be due to invalid IL or missing references) - //IL_012f: Unknown result type (might be due to invalid IL or missing references) - //IL_0139: Unknown result type (might be due to invalid IL or missing references) - //IL_0144: Unknown result type (might be due to invalid IL or missing references) - //IL_014d: Expected O, but got Unknown - //IL_0182: Unknown result type (might be due to invalid IL or missing references) - //IL_0187: Unknown result type (might be due to invalid IL or missing references) - //IL_0189: Unknown result type (might be due to invalid IL or missing references) - //IL_0193: Unknown result type (might be due to invalid IL or missing references) - //IL_019e: Unknown result type (might be due to invalid IL or missing references) - //IL_01a7: Expected O, but got Unknown - string text = Text; - SKRect val = default(SKRect); - paint.MeasureText(text, ref val); - if (((SKRect)(ref val)).Width > ((SKRect)(ref bounds)).Width && LineBreakMode == LineBreakMode.TailTruncation) - { - text = TruncateText(paint, text, ((SKRect)(ref bounds)).Width); - paint.MeasureText(text, ref val); - } - float num = HorizontalTextAlignment switch - { - TextAlignment.Start => ((SKRect)(ref bounds)).Left, - TextAlignment.Center => ((SKRect)(ref bounds)).MidX - ((SKRect)(ref val)).Width / 2f, - TextAlignment.End => ((SKRect)(ref bounds)).Right - ((SKRect)(ref val)).Width, - _ => ((SKRect)(ref bounds)).Left, - }; - float num2 = VerticalTextAlignment switch - { - TextAlignment.Start => ((SKRect)(ref bounds)).Top - ((SKRect)(ref val)).Top, - TextAlignment.Center => ((SKRect)(ref bounds)).MidY - ((SKRect)(ref val)).MidY, - TextAlignment.End => ((SKRect)(ref bounds)).Bottom - ((SKRect)(ref val)).Bottom, - _ => ((SKRect)(ref bounds)).MidY - ((SKRect)(ref val)).MidY, - }; - canvas.DrawText(text, num, num2, paint); - if (IsUnderline) - { - SKPaint val2 = new SKPaint - { - Color = paint.Color, - StrokeWidth = 1f, - IsAntialias = true - }; - try - { - float num3 = num2 + 2f; - canvas.DrawLine(num, num3, num + ((SKRect)(ref val)).Width, num3, val2); - } - finally - { - ((IDisposable)val2)?.Dispose(); - } - } - if (IsStrikethrough) - { - SKPaint val3 = new SKPaint - { - Color = paint.Color, - StrokeWidth = 1f, - IsAntialias = true - }; - try - { - float num4 = num2 - ((SKRect)(ref val)).Height / 3f; - canvas.DrawLine(num, num4, num + ((SKRect)(ref val)).Width, num4, val3); - } - finally - { - ((IDisposable)val3)?.Dispose(); - } - } - } + var fontStyle = new SKFontStyle( + IsBold ? SKFontStyleWeight.Bold : SKFontStyleWeight.Normal, + SKFontStyleWidth.Normal, + IsItalic ? SKFontStyleSlant.Italic : SKFontStyleSlant.Upright); - private void DrawMultiLineWithWrapping(SKCanvas canvas, SKPaint paint, SKFont font, SKRect bounds) - { - //IL_0000: Unknown result type (might be due to invalid IL or missing references) - //IL_0002: Unknown result type (might be due to invalid IL or missing references) - //IL_01ab: Unknown result type (might be due to invalid IL or missing references) - SKRect val = bounds; - if (((SKRect)(ref bounds)).Height <= 0f) - { - float num = ((LineHeight <= 0f) ? 1.2f : LineHeight); - float num2 = ((MaxLines > 0) ? ((float)MaxLines * FontSize * num) : (FontSize * num * 10f)); - ((SKRect)(ref val))._002Ector(((SKRect)(ref bounds)).Left, ((SKRect)(ref bounds)).Top, ((SKRect)(ref bounds)).Right, ((SKRect)(ref bounds)).Top + num2); - } - float num3 = ((SKRect)(ref val)).Width; - if (num3 <= 0f) - { - num3 = 400f; - } - List list = WrapText(paint, Text, num3); - float num4 = ((LineHeight <= 0f) ? 1.2f : LineHeight); - float num5 = FontSize * num4; - int num6 = ((MaxLines > 0) ? Math.Min(MaxLines, list.Count) : list.Count); - float num7 = (float)num6 * num5; - float num8 = VerticalTextAlignment switch - { - TextAlignment.Start => ((SKRect)(ref val)).Top + FontSize, - TextAlignment.Center => ((SKRect)(ref val)).MidY - num7 / 2f + FontSize, - TextAlignment.End => ((SKRect)(ref val)).Bottom - num7 + FontSize, - _ => ((SKRect)(ref val)).Top + FontSize, - }; - for (int i = 0; i < num6; i++) - { - string text = list[i]; - bool num9 = i == num6 - 1; - bool flag = num6 < list.Count; - if (num9 && flag && LineBreakMode == LineBreakMode.TailTruncation) - { - text = TruncateTextWithEllipsis(paint, text, num3); - } - SKRect val2 = default(SKRect); - paint.MeasureText(text, ref val2); - float num10 = HorizontalTextAlignment switch - { - TextAlignment.Start => ((SKRect)(ref val)).Left, - TextAlignment.Center => ((SKRect)(ref val)).MidX - ((SKRect)(ref val2)).Width / 2f, - TextAlignment.End => ((SKRect)(ref val)).Right - ((SKRect)(ref val2)).Width, - _ => ((SKRect)(ref val)).Left, - }; - float num11 = num8 + (float)i * num5; - if (!(((SKRect)(ref val)).Height > 0f) || !(num11 > ((SKRect)(ref val)).Bottom)) - { - canvas.DrawText(text, num10, num11, paint); - continue; - } - break; - } - } + var typeface = SkiaRenderingEngine.Current?.ResourceCache.GetTypeface(FontFamily, fontStyle); + if (typeface == null || typeface == SKTypeface.Default) + { + typeface = GetLinuxTypeface(); + } - private List WrapText(SKPaint paint, string text, float maxWidth) - { - List list = new List(); - string[] array = text.Split('\n'); - foreach (string text2 in array) - { - if (string.IsNullOrEmpty(text2)) - { - list.Add(""); - continue; - } - if (paint.MeasureText(text2) <= maxWidth) - { - list.Add(text2); - continue; - } - string[] array2 = text2.Split(' '); - string text3 = ""; - string[] array3 = array2; - foreach (string text4 in array3) - { - string text5 = (string.IsNullOrEmpty(text3) ? text4 : (text3 + " " + text4)); - if (paint.MeasureText(text5) > maxWidth && !string.IsNullOrEmpty(text3)) - { - list.Add(text3); - text3 = text4; - } - else - { - text3 = text5; - } - } - if (!string.IsNullOrEmpty(text3)) - { - list.Add(text3); - } - } - return list; - } + using var font = new SKFont(typeface, FontSize); + using var paint = new SKPaint(font) + { + Color = IsEnabled ? TextColor : TextColor.WithAlpha(128), + IsAntialias = true + }; - private void DrawMultiLine(SKCanvas canvas, SKPaint paint, SKFont font, SKRect bounds) - { - //IL_0104: Unknown result type (might be due to invalid IL or missing references) - string[] array = Text.Split('\n'); - float num = ((LineHeight <= 0f) ? 1.2f : LineHeight); - float num2 = FontSize * num; - int num3 = ((MaxLines > 0) ? Math.Min(MaxLines, array.Length) : array.Length); - float num4 = (float)num3 * num2; - float num5 = VerticalTextAlignment switch - { - TextAlignment.Start => ((SKRect)(ref bounds)).Top + FontSize, - TextAlignment.Center => ((SKRect)(ref bounds)).MidY - num4 / 2f + FontSize, - TextAlignment.End => ((SKRect)(ref bounds)).Bottom - num4 + FontSize, - _ => ((SKRect)(ref bounds)).Top + FontSize, - }; - for (int i = 0; i < num3; i++) - { - string text = array[i]; - if (i == num3 - 1 && i < array.Length - 1 && LineBreakMode == LineBreakMode.TailTruncation) - { - text = TruncateText(paint, text, ((SKRect)(ref bounds)).Width); - } - SKRect val = default(SKRect); - paint.MeasureText(text, ref val); - float num6 = HorizontalTextAlignment switch - { - TextAlignment.Start => ((SKRect)(ref bounds)).Left, - TextAlignment.Center => ((SKRect)(ref bounds)).MidX - ((SKRect)(ref val)).Width / 2f, - TextAlignment.End => ((SKRect)(ref bounds)).Right - ((SKRect)(ref val)).Width, - _ => ((SKRect)(ref bounds)).Left, - }; - float num7 = num5 + (float)i * num2; - if (!(num7 > ((SKRect)(ref bounds)).Bottom)) - { - canvas.DrawText(text, num6, num7, paint); - continue; - } - break; - } - } + // Calculate content bounds with padding + var contentBounds = new SKRect( + bounds.Left + Padding.Left, + bounds.Top + Padding.Top, + bounds.Right - Padding.Right, + bounds.Bottom - Padding.Bottom); - private string TruncateTextWithEllipsis(SKPaint paint, string text, float maxWidth) - { - float num = paint.MeasureText("..."); - if (paint.MeasureText(text) + num <= maxWidth) - { - return text + "..."; - } - float num2 = maxWidth - num; - if (num2 <= 0f) - { - return "..."; - } - int num3 = 0; - int num4 = text.Length; - while (num3 < num4) - { - int num5 = (num3 + num4 + 1) / 2; - string text2 = text.Substring(0, num5); - if (paint.MeasureText(text2) <= num2) - { - num3 = num5; - } - else - { - num4 = num5 - 1; - } - } - return text.Substring(0, num3) + "..."; - } + // Handle single line vs multiline + // Use DrawMultiLineWithWrapping when: MaxLines > 1, text has newlines, OR WordWrap is enabled + bool needsMultiLine = MaxLines > 1 || Text.Contains('\n') || + LineBreakMode == LineBreakMode.WordWrap || + LineBreakMode == LineBreakMode.CharacterWrap; + if (needsMultiLine) + { + DrawMultiLineWithWrapping(canvas, paint, font, contentBounds); + } + else + { + DrawSingleLine(canvas, paint, font, contentBounds); + } + } - private string TruncateText(SKPaint paint, string text, float maxWidth) - { - float num = paint.MeasureText("..."); - if (paint.MeasureText(text) <= maxWidth) - { - return text; - } - float num2 = maxWidth - num; - if (num2 <= 0f) - { - return "..."; - } - int num3 = 0; - int num4 = text.Length; - while (num3 < num4) - { - int num5 = (num3 + num4 + 1) / 2; - string text2 = text.Substring(0, num5); - if (paint.MeasureText(text2) <= num2) - { - num3 = num5; - } - else - { - num4 = num5 - 1; - } - } - return text.Substring(0, num3) + "..."; - } + private void DrawSingleLine(SKCanvas canvas, SKPaint paint, SKFont font, SKRect bounds) + { + var displayText = Text; - protected override SKSize MeasureOverride(SKSize availableSize) - { - //IL_000e: Unknown result type (might be due to invalid IL or missing references) - //IL_0013: Unknown result type (might be due to invalid IL or missing references) - //IL_001d: Unknown result type (might be due to invalid IL or missing references) - //IL_0022: Unknown result type (might be due to invalid IL or missing references) - //IL_0033: Unknown result type (might be due to invalid IL or missing references) - //IL_0038: Unknown result type (might be due to invalid IL or missing references) - //IL_0043: Unknown result type (might be due to invalid IL or missing references) - //IL_0048: Unknown result type (might be due to invalid IL or missing references) - //IL_0052: Unknown result type (might be due to invalid IL or missing references) - //IL_0079: Unknown result type (might be due to invalid IL or missing references) - //IL_007f: Expected O, but got Unknown - //IL_00bf: Unknown result type (might be due to invalid IL or missing references) - //IL_00c5: Expected O, but got Unknown - //IL_00c6: Unknown result type (might be due to invalid IL or missing references) - //IL_00cc: Expected O, but got Unknown - //IL_017a: Unknown result type (might be due to invalid IL or missing references) - //IL_017f: Unknown result type (might be due to invalid IL or missing references) - //IL_018a: Unknown result type (might be due to invalid IL or missing references) - //IL_018f: Unknown result type (might be due to invalid IL or missing references) - //IL_00fd: Unknown result type (might be due to invalid IL or missing references) - //IL_011a: Unknown result type (might be due to invalid IL or missing references) - //IL_011f: Unknown result type (might be due to invalid IL or missing references) - //IL_012a: Unknown result type (might be due to invalid IL or missing references) - //IL_012f: Unknown result type (might be due to invalid IL or missing references) - //IL_0147: Unknown result type (might be due to invalid IL or missing references) - //IL_014c: Unknown result type (might be due to invalid IL or missing references) - //IL_0157: Unknown result type (might be due to invalid IL or missing references) - //IL_015c: Unknown result type (might be due to invalid IL or missing references) - //IL_0166: Unknown result type (might be due to invalid IL or missing references) - //IL_016b: Unknown result type (might be due to invalid IL or missing references) - //IL_02b8: Unknown result type (might be due to invalid IL or missing references) - //IL_025a: Unknown result type (might be due to invalid IL or missing references) - //IL_025f: Unknown result type (might be due to invalid IL or missing references) - //IL_026a: Unknown result type (might be due to invalid IL or missing references) - //IL_026f: Unknown result type (might be due to invalid IL or missing references) - //IL_027c: Unknown result type (might be due to invalid IL or missing references) - //IL_0281: Unknown result type (might be due to invalid IL or missing references) - //IL_028c: Unknown result type (might be due to invalid IL or missing references) - //IL_0291: Unknown result type (might be due to invalid IL or missing references) - //IL_029b: Unknown result type (might be due to invalid IL or missing references) - //IL_02a0: Unknown result type (might be due to invalid IL or missing references) - SKRect padding; - if (string.IsNullOrEmpty(Text)) - { - padding = Padding; - float left = ((SKRect)(ref padding)).Left; - padding = Padding; - float num = left + ((SKRect)(ref padding)).Right; - float fontSize = FontSize; - padding = Padding; - float num2 = fontSize + ((SKRect)(ref padding)).Top; - padding = Padding; - return new SKSize(num, num2 + ((SKRect)(ref padding)).Bottom); - } - SKFontStyle style = new SKFontStyle((SKFontStyleWeight)(IsBold ? 700 : 400), (SKFontStyleWidth)5, (SKFontStyleSlant)(IsItalic ? 1 : 0)); - SKTypeface val = SkiaRenderingEngine.Current?.ResourceCache.GetTypeface(FontFamily, style); - if (val == null || val == SKTypeface.Default) - { - val = GetLinuxTypeface(); - } - SKFont val2 = new SKFont(val, FontSize, 1f, 0f); - try - { - SKPaint val3 = new SKPaint(val2); - try - { - if (MaxLines <= 1 && !Text.Contains('\n') && LineBreakMode != LineBreakMode.WordWrap && LineBreakMode != LineBreakMode.CharacterWrap) - { - SKRect val4 = default(SKRect); - val3.MeasureText(Text, ref val4); - float width = ((SKRect)(ref val4)).Width; - padding = Padding; - float num3 = width + ((SKRect)(ref padding)).Left; - padding = Padding; - float num4 = num3 + ((SKRect)(ref padding)).Right + 4f; - float height = ((SKRect)(ref val4)).Height; - padding = Padding; - float num5 = height + ((SKRect)(ref padding)).Top; - padding = Padding; - return new SKSize(num4, num5 + ((SKRect)(ref padding)).Bottom); - } - float width2 = ((SKSize)(ref availableSize)).Width; - padding = Padding; - float num6 = width2 - ((SKRect)(ref padding)).Left; - padding = Padding; - float num7 = num6 - ((SKRect)(ref padding)).Right; - if (num7 <= 0f) - { - num7 = float.MaxValue; - } - List list = WrapText(val3, Text, num7); - int num8 = ((MaxLines > 0) ? Math.Min(MaxLines, list.Count) : list.Count); - float num9 = 0f; - foreach (string item in list.Take(num8)) - { - num9 = Math.Max(num9, val3.MeasureText(item)); - } - float num10 = ((LineHeight <= 0f) ? 1.2f : LineHeight); - float num11 = (float)num8 * FontSize * num10; - float num12 = num9; - padding = Padding; - float num13 = num12 + ((SKRect)(ref padding)).Left; - padding = Padding; - float num14 = num13 + ((SKRect)(ref padding)).Right; - padding = Padding; - float num15 = num11 + ((SKRect)(ref padding)).Top; - padding = Padding; - return new SKSize(num14, num15 + ((SKRect)(ref padding)).Bottom); - } - finally - { - ((IDisposable)val3)?.Dispose(); - } - } - finally - { - ((IDisposable)val2)?.Dispose(); - } - } + // Measure text + var textBounds = new SKRect(); + paint.MeasureText(displayText, ref textBounds); - public override void OnPointerPressed(PointerEventArgs e) - { - base.OnPointerPressed(e); - this.Tapped?.Invoke(this, EventArgs.Empty); - } + // Apply truncation if needed + if (textBounds.Width > bounds.Width && LineBreakMode == LineBreakMode.TailTruncation) + { + displayText = TruncateText(paint, displayText, bounds.Width); + paint.MeasureText(displayText, ref textBounds); + } + + // Calculate position based on alignment + float x = HorizontalTextAlignment switch + { + TextAlignment.Start => bounds.Left, + TextAlignment.Center => bounds.MidX - textBounds.Width / 2, + TextAlignment.End => bounds.Right - textBounds.Width, + _ => bounds.Left + }; + + float y = VerticalTextAlignment switch + { + TextAlignment.Start => bounds.Top - textBounds.Top, + TextAlignment.Center => bounds.MidY - textBounds.MidY, + TextAlignment.End => bounds.Bottom - textBounds.Bottom, + _ => bounds.MidY - textBounds.MidY + }; + + canvas.DrawText(displayText, x, y, paint); + + // Draw underline if needed + if (IsUnderline) + { + using var linePaint = new SKPaint + { + Color = paint.Color, + StrokeWidth = 1, + IsAntialias = true + }; + var underlineY = y + 2; + canvas.DrawLine(x, underlineY, x + textBounds.Width, underlineY, linePaint); + } + + // Draw strikethrough if needed + if (IsStrikethrough) + { + using var linePaint = new SKPaint + { + Color = paint.Color, + StrokeWidth = 1, + IsAntialias = true + }; + var strikeY = y - textBounds.Height / 3; + canvas.DrawLine(x, strikeY, x + textBounds.Width, strikeY, linePaint); + } + } + + private void DrawMultiLineWithWrapping(SKCanvas canvas, SKPaint paint, SKFont font, SKRect bounds) + { + // Handle inverted or zero-height/width bounds + var effectiveBounds = bounds; + + // Fix invalid height + if (bounds.Height <= 0) + { + var effectiveLH = LineHeight <= 0 ? 1.2f : LineHeight; + var estimatedHeight = MaxLines > 0 ? MaxLines * FontSize * effectiveLH : FontSize * effectiveLH * 10; + effectiveBounds = new SKRect(bounds.Left, bounds.Top, bounds.Right, bounds.Top + estimatedHeight); + } + + // Fix invalid width - use a reasonable default if width is invalid or extremely large + float effectiveWidth = effectiveBounds.Width; + if (effectiveWidth <= 0) + { + // Use a default width based on canvas + effectiveWidth = 400; // Reasonable default + } + + // Note: Previously had width capping logic here that reduced effective width + // to 60% for multiline labels. Removed - the layout system should now provide + // correct widths, and artificially capping causes text to wrap too early. + + // First, word-wrap the text to fit within bounds + var wrappedLines = WrapText(paint, Text, effectiveWidth); + + // LineHeight of -1 or <= 0 means "use default" - use 1.2 as default multiplier + var effectiveLineHeight = LineHeight <= 0 ? 1.2f : LineHeight; + var lineSpacing = FontSize * effectiveLineHeight; + var maxLinesToDraw = MaxLines > 0 ? Math.Min(MaxLines, wrappedLines.Count) : wrappedLines.Count; + + // Calculate total height + var totalHeight = maxLinesToDraw * lineSpacing; + + // Calculate starting Y based on vertical alignment + float startY = VerticalTextAlignment switch + { + TextAlignment.Start => effectiveBounds.Top + FontSize, + TextAlignment.Center => effectiveBounds.MidY - totalHeight / 2 + FontSize, + TextAlignment.End => effectiveBounds.Bottom - totalHeight + FontSize, + _ => effectiveBounds.Top + FontSize + }; + + for (int i = 0; i < maxLinesToDraw; i++) + { + var line = wrappedLines[i]; + + // Add ellipsis if this is the last line and there are more lines + bool isLastLine = i == maxLinesToDraw - 1; + bool hasMoreContent = maxLinesToDraw < wrappedLines.Count; + if (isLastLine && hasMoreContent && LineBreakMode == LineBreakMode.TailTruncation) + { + line = TruncateTextWithEllipsis(paint, line, effectiveWidth); + } + + var textBounds = new SKRect(); + paint.MeasureText(line, ref textBounds); + + float x = HorizontalTextAlignment switch + { + TextAlignment.Start => effectiveBounds.Left, + TextAlignment.Center => effectiveBounds.MidX - textBounds.Width / 2, + TextAlignment.End => effectiveBounds.Right - textBounds.Width, + _ => effectiveBounds.Left + }; + + float y = startY + i * lineSpacing; + + // Don't break early for inverted bounds - just draw + if (effectiveBounds.Height > 0 && y > effectiveBounds.Bottom) + break; + + canvas.DrawText(line, x, y, paint); + } + } + + private List WrapText(SKPaint paint, string text, float maxWidth) + { + var result = new List(); + + // Split by newlines first + var paragraphs = text.Split('\n'); + + foreach (var paragraph in paragraphs) + { + if (string.IsNullOrEmpty(paragraph)) + { + result.Add(""); + continue; + } + + // Check if paragraph fits in one line + if (paint.MeasureText(paragraph) <= maxWidth) + { + result.Add(paragraph); + continue; + } + + // Word wrap this paragraph + var words = paragraph.Split(' '); + var currentLine = ""; + + foreach (var word in words) + { + var testLine = string.IsNullOrEmpty(currentLine) ? word : currentLine + " " + word; + var lineWidth = paint.MeasureText(testLine); + + if (lineWidth > maxWidth && !string.IsNullOrEmpty(currentLine)) + { + result.Add(currentLine); + currentLine = word; + } + else + { + currentLine = testLine; + } + } + + if (!string.IsNullOrEmpty(currentLine)) + { + result.Add(currentLine); + } + } + + return result; + } + + private void DrawMultiLine(SKCanvas canvas, SKPaint paint, SKFont font, SKRect bounds) + { + var lines = Text.Split('\n'); + var effectiveLineHeight = LineHeight <= 0 ? 1.2f : LineHeight; + var lineSpacing = FontSize * effectiveLineHeight; + var maxLinesToDraw = MaxLines > 0 ? Math.Min(MaxLines, lines.Length) : lines.Length; + + // Calculate total height + var totalHeight = maxLinesToDraw * lineSpacing; + + // Calculate starting Y based on vertical alignment + float startY = VerticalTextAlignment switch + { + TextAlignment.Start => bounds.Top + FontSize, + TextAlignment.Center => bounds.MidY - totalHeight / 2 + FontSize, + TextAlignment.End => bounds.Bottom - totalHeight + FontSize, + _ => bounds.Top + FontSize + }; + + for (int i = 0; i < maxLinesToDraw; i++) + { + var line = lines[i]; + + // Add ellipsis if this is the last line and there are more + if (i == maxLinesToDraw - 1 && i < lines.Length - 1 && LineBreakMode == LineBreakMode.TailTruncation) + { + line = TruncateText(paint, line, bounds.Width); + } + + var textBounds = new SKRect(); + paint.MeasureText(line, ref textBounds); + + float x = HorizontalTextAlignment switch + { + TextAlignment.Start => bounds.Left, + TextAlignment.Center => bounds.MidX - textBounds.Width / 2, + TextAlignment.End => bounds.Right - textBounds.Width, + _ => bounds.Left + }; + + float y = startY + i * lineSpacing; + + if (y > bounds.Bottom) + break; + + canvas.DrawText(line, x, y, paint); + } + } + + /// + /// Truncates text and ALWAYS adds ellipsis (used when there's more content to indicate). + /// + private string TruncateTextWithEllipsis(SKPaint paint, string text, float maxWidth) + { + const string ellipsis = "..."; + var ellipsisWidth = paint.MeasureText(ellipsis); + var textWidth = paint.MeasureText(text); + + // If text + ellipsis fits, just append ellipsis + if (textWidth + ellipsisWidth <= maxWidth) + return text + ellipsis; + + // Otherwise, truncate to make room for ellipsis + var availableWidth = maxWidth - ellipsisWidth; + if (availableWidth <= 0) + return ellipsis; + + // Binary search for the right length + int low = 0; + int high = text.Length; + + while (low < high) + { + int mid = (low + high + 1) / 2; + var substring = text.Substring(0, mid); + + if (paint.MeasureText(substring) <= availableWidth) + low = mid; + else + high = mid - 1; + } + + return text.Substring(0, low) + ellipsis; + } + + private string TruncateText(SKPaint paint, string text, float maxWidth) + { + const string ellipsis = "..."; + var ellipsisWidth = paint.MeasureText(ellipsis); + + if (paint.MeasureText(text) <= maxWidth) + return text; + + var availableWidth = maxWidth - ellipsisWidth; + if (availableWidth <= 0) + return ellipsis; + + // Binary search for the right length + int low = 0; + int high = text.Length; + + while (low < high) + { + int mid = (low + high + 1) / 2; + var substring = text.Substring(0, mid); + + if (paint.MeasureText(substring) <= availableWidth) + low = mid; + else + high = mid - 1; + } + + return text.Substring(0, low) + ellipsis; + } + + protected override SKSize MeasureOverride(SKSize availableSize) + { + if (string.IsNullOrEmpty(Text)) + { + return new SKSize( + Padding.Left + Padding.Right, + FontSize + Padding.Top + Padding.Bottom); + } + + var fontStyle = new SKFontStyle( + IsBold ? SKFontStyleWeight.Bold : SKFontStyleWeight.Normal, + SKFontStyleWidth.Normal, + IsItalic ? SKFontStyleSlant.Italic : SKFontStyleSlant.Upright); + + // Use same typeface logic as OnDraw to ensure consistent measurement + var typeface = SkiaRenderingEngine.Current?.ResourceCache.GetTypeface(FontFamily, fontStyle); + if (typeface == null || typeface == SKTypeface.Default) + { + typeface = GetLinuxTypeface(); + } + + using var font = new SKFont(typeface, FontSize); + using var paint = new SKPaint(font); + + // Use multiline when: MaxLines > 1, text has newlines, OR WordWrap is enabled + bool needsMultiLine = MaxLines > 1 || Text.Contains('\n') || + LineBreakMode == LineBreakMode.WordWrap || + LineBreakMode == LineBreakMode.CharacterWrap; + if (!needsMultiLine) + { + var textBounds = new SKRect(); + paint.MeasureText(Text, ref textBounds); + + // Add small buffer for font rendering tolerance + const float widthBuffer = 4f; + + return new SKSize( + textBounds.Width + Padding.Left + Padding.Right + widthBuffer, + textBounds.Height + Padding.Top + Padding.Bottom); + } + else + { + // Use available width for word wrapping measurement + var wrapWidth = availableSize.Width - Padding.Left - Padding.Right; + if (wrapWidth <= 0) + { + wrapWidth = float.MaxValue; // No wrapping if no width constraint + } + + // Wrap text to get actual line count + var wrappedLines = WrapText(paint, Text, wrapWidth); + var maxLinesToMeasure = MaxLines > 0 ? Math.Min(MaxLines, wrappedLines.Count) : wrappedLines.Count; + + float maxWidth = 0; + foreach (var line in wrappedLines.Take(maxLinesToMeasure)) + { + maxWidth = Math.Max(maxWidth, paint.MeasureText(line)); + } + + var effectiveLineHeight = LineHeight <= 0 ? 1.2f : LineHeight; + var totalHeight = maxLinesToMeasure * FontSize * effectiveLineHeight; + + return new SKSize( + maxWidth + Padding.Left + Padding.Right, + totalHeight + Padding.Top + Padding.Bottom); + } + } +} + +/// +/// Text alignment options. +/// +public enum TextAlignment +{ + Start, + Center, + End +} + +/// +/// Line break mode options. +/// +public enum LineBreakMode +{ + NoWrap, + WordWrap, + CharacterWrap, + HeadTruncation, + TailTruncation, + MiddleTruncation +} + +/// +/// Horizontal text alignment for Skia label. +/// +public enum SkiaTextAlignment +{ + Left, + Center, + Right +} + +/// +/// Vertical text alignment for Skia label. +/// +public enum SkiaVerticalAlignment +{ + Top, + Center, + Bottom } diff --git a/Views/SkiaLayoutView.cs b/Views/SkiaLayoutView.cs index 7942273..1cc58ba 100644 --- a/Views/SkiaLayoutView.cs +++ b/Views/SkiaLayoutView.cs @@ -1,303 +1,1127 @@ -using System; -using System.Collections.Generic; -using Microsoft.Maui.Controls; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using SkiaSharp; +using Microsoft.Maui; namespace Microsoft.Maui.Platform; +/// +/// Base class for layout containers that can arrange child views. +/// public abstract class SkiaLayoutView : SkiaView { - public static readonly BindableProperty SpacingProperty = BindableProperty.Create("Spacing", typeof(float), typeof(SkiaLayoutView), (object)0f, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaLayoutView)(object)b).InvalidateMeasure(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + #region BindableProperties - public static readonly BindableProperty PaddingProperty = BindableProperty.Create("Padding", typeof(SKRect), typeof(SkiaLayoutView), (object)SKRect.Empty, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaLayoutView)(object)b).InvalidateMeasure(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for Spacing. + /// + public static readonly BindableProperty SpacingProperty = + BindableProperty.Create( + nameof(Spacing), + typeof(float), + typeof(SkiaLayoutView), + 0f, + propertyChanged: (b, o, n) => ((SkiaLayoutView)b).InvalidateMeasure()); - public static readonly BindableProperty ClipToBoundsProperty = BindableProperty.Create("ClipToBounds", typeof(bool), typeof(SkiaLayoutView), (object)false, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaLayoutView)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for Padding. + /// + public static readonly BindableProperty PaddingProperty = + BindableProperty.Create( + nameof(Padding), + typeof(SKRect), + typeof(SkiaLayoutView), + SKRect.Empty, + propertyChanged: (b, o, n) => ((SkiaLayoutView)b).InvalidateMeasure()); - private readonly List _children = new List(); + /// + /// Bindable property for ClipToBounds. + /// + public static readonly BindableProperty ClipToBoundsProperty = + BindableProperty.Create( + nameof(ClipToBounds), + typeof(bool), + typeof(SkiaLayoutView), + false, + propertyChanged: (b, o, n) => ((SkiaLayoutView)b).Invalidate()); - public new IReadOnlyList Children => _children; + #endregion - public float Spacing - { - get - { - return (float)((BindableObject)this).GetValue(SpacingProperty); - } - set - { - ((BindableObject)this).SetValue(SpacingProperty, (object)value); - } - } + private readonly List _children = new(); - public SKRect Padding - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKRect)((BindableObject)this).GetValue(PaddingProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(PaddingProperty, (object)value); - } - } + /// + /// Gets the children of this layout. + /// + public new IReadOnlyList Children => _children; - public bool ClipToBounds - { - get - { - return (bool)((BindableObject)this).GetValue(ClipToBoundsProperty); - } - set - { - ((BindableObject)this).SetValue(ClipToBoundsProperty, (object)value); - } - } + /// + /// Spacing between children. + /// + public float Spacing + { + get => (float)GetValue(SpacingProperty); + set => SetValue(SpacingProperty, value); + } - protected override void OnBindingContextChanged() - { - base.OnBindingContextChanged(); - foreach (SkiaView child in _children) - { - BindableObject.SetInheritedBindingContext((BindableObject)(object)child, ((BindableObject)this).BindingContext); - } - } + /// + /// Padding around the content. + /// + public SKRect Padding + { + get => (SKRect)GetValue(PaddingProperty); + set => SetValue(PaddingProperty, value); + } - public new virtual void AddChild(SkiaView child) - { - if (child.Parent != null) - { - throw new InvalidOperationException("View already has a parent"); - } - _children.Add(child); - child.Parent = this; - if (((BindableObject)this).BindingContext != null) - { - BindableObject.SetInheritedBindingContext((BindableObject)(object)child, ((BindableObject)this).BindingContext); - } - InvalidateMeasure(); - Invalidate(); - } + /// + /// Gets or sets whether child views are clipped to the bounds. + /// + public bool ClipToBounds + { + get => (bool)GetValue(ClipToBoundsProperty); + set => SetValue(ClipToBoundsProperty, value); + } - public new virtual void RemoveChild(SkiaView child) - { - if (_children.Remove(child)) - { - child.Parent = null; - InvalidateMeasure(); - Invalidate(); - } - } + /// + /// Called when binding context changes. Propagates to layout children. + /// + protected override void OnBindingContextChanged() + { + base.OnBindingContextChanged(); - public virtual void RemoveChildAt(int index) - { - if (index >= 0 && index < _children.Count) - { - SkiaView skiaView = _children[index]; - _children.RemoveAt(index); - skiaView.Parent = null; - InvalidateMeasure(); - Invalidate(); - } - } + // Propagate binding context to layout children + foreach (var child in _children) + { + SetInheritedBindingContext(child, BindingContext); + } + } - public new virtual void InsertChild(int index, SkiaView child) - { - if (child.Parent != null) - { - throw new InvalidOperationException("View already has a parent"); - } - _children.Insert(index, child); - child.Parent = this; - if (((BindableObject)this).BindingContext != null) - { - BindableObject.SetInheritedBindingContext((BindableObject)(object)child, ((BindableObject)this).BindingContext); - } - InvalidateMeasure(); - Invalidate(); - } + /// + /// Adds a child view. + /// + public virtual void AddChild(SkiaView child) + { + if (child.Parent != null) + { + throw new InvalidOperationException("View already has a parent"); + } - public new virtual void ClearChildren() - { - foreach (SkiaView child in _children) - { - child.Parent = null; - } - _children.Clear(); - InvalidateMeasure(); - Invalidate(); - } + _children.Add(child); + child.Parent = this; - protected virtual SKRect GetContentBounds() - { - //IL_0002: Unknown result type (might be due to invalid IL or missing references) - //IL_0007: Unknown result type (might be due to invalid IL or missing references) - return GetContentBounds(base.Bounds); - } + // Propagate binding context to new child + if (BindingContext != null) + { + SetInheritedBindingContext(child, BindingContext); + } - protected SKRect GetContentBounds(SKRect bounds) - { - //IL_0008: Unknown result type (might be due to invalid IL or missing references) - //IL_000d: Unknown result type (might be due to invalid IL or missing references) - //IL_001e: Unknown result type (might be due to invalid IL or missing references) - //IL_0023: Unknown result type (might be due to invalid IL or missing references) - //IL_0034: Unknown result type (might be due to invalid IL or missing references) - //IL_0039: Unknown result type (might be due to invalid IL or missing references) - //IL_004a: Unknown result type (might be due to invalid IL or missing references) - //IL_004f: Unknown result type (might be due to invalid IL or missing references) - //IL_0058: Unknown result type (might be due to invalid IL or missing references) - float left = ((SKRect)(ref bounds)).Left; - SKRect padding = Padding; - float num = left + ((SKRect)(ref padding)).Left; - float top = ((SKRect)(ref bounds)).Top; - padding = Padding; - float num2 = top + ((SKRect)(ref padding)).Top; - float right = ((SKRect)(ref bounds)).Right; - padding = Padding; - float num3 = right - ((SKRect)(ref padding)).Right; - float bottom = ((SKRect)(ref bounds)).Bottom; - padding = Padding; - return new SKRect(num, num2, num3, bottom - ((SKRect)(ref padding)).Bottom); - } + InvalidateMeasure(); + Invalidate(); + } - protected override void OnDraw(SKCanvas canvas, SKRect bounds) - { - //IL_0001: Unknown result type (might be due to invalid IL or missing references) - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - //IL_0012: Unknown result type (might be due to invalid IL or missing references) - //IL_0017: Unknown result type (might be due to invalid IL or missing references) - //IL_0019: Unknown result type (might be due to invalid IL or missing references) - //IL_0023: Unknown result type (might be due to invalid IL or missing references) - //IL_002b: Expected O, but got Unknown - //IL_002c: Unknown result type (might be due to invalid IL or missing references) - //IL_00a1: Unknown result type (might be due to invalid IL or missing references) - //IL_013a: Unknown result type (might be due to invalid IL or missing references) - if (base.BackgroundColor != SKColors.Transparent) - { - SKPaint val = new SKPaint - { - Color = base.BackgroundColor, - Style = (SKPaintStyle)0 - }; - try - { - canvas.DrawRect(bounds, val); - } - finally - { - ((IDisposable)val)?.Dispose(); - } - } - if (this is SkiaStackLayout) - { - bool flag = false; - foreach (SkiaView child in _children) - { - if (child is SkiaCollectionView) - { - flag = true; - } - } - if (flag) - { - Console.WriteLine($"[SkiaStackLayout+CV] OnDraw - bounds={bounds}, children={_children.Count}"); - foreach (SkiaView child2 in _children) - { - Console.WriteLine($"[SkiaStackLayout+CV] Child: {((object)child2).GetType().Name}, IsVisible={child2.IsVisible}, Bounds={child2.Bounds}"); - } - } - } - foreach (SkiaView child3 in _children) - { - if (child3.IsVisible) - { - child3.Draw(canvas); - } - } - } + /// + /// Removes a child view. + /// + public virtual void RemoveChild(SkiaView child) + { + if (_children.Remove(child)) + { + child.Parent = null; + InvalidateMeasure(); + Invalidate(); + } + } - public override SkiaView? HitTest(float x, float y) - { - //IL_0076: Unknown result type (might be due to invalid IL or missing references) - //IL_0011: Unknown result type (might be due to invalid IL or missing references) - //IL_0016: Unknown result type (might be due to invalid IL or missing references) - //IL_001b: Unknown result type (might be due to invalid IL or missing references) - //IL_01bd: Unknown result type (might be due to invalid IL or missing references) - //IL_0133: Unknown result type (might be due to invalid IL or missing references) - if (base.IsVisible && base.IsEnabled) - { - SKRect bounds = base.Bounds; - if (((SKRect)(ref bounds)).Contains(new SKPoint(x, y))) - { - for (int num = _children.Count - 1; num >= 0; num--) - { - SkiaView skiaView = _children[num].HitTest(x, y); - if (skiaView != null) - { - if (this is SkiaBorder) - { - Console.WriteLine($"[SkiaBorder.HitTest] Hit child - x={x}, y={y}, Bounds={base.Bounds}, child={((object)skiaView).GetType().Name}"); - } - return skiaView; - } - } - if (this is SkiaBorder) - { - Console.WriteLine($"[SkiaBorder.HitTest] Hit self - x={x}, y={y}, Bounds={base.Bounds}, children={_children.Count}"); - } - return this; - } - } - if (this is SkiaBorder) - { - Console.WriteLine($"[SkiaBorder.HitTest] Miss - x={x}, y={y}, Bounds={base.Bounds}, IsVisible={base.IsVisible}, IsEnabled={base.IsEnabled}"); - } - return null; - } + /// + /// Removes a child at the specified index. + /// + public virtual void RemoveChildAt(int index) + { + if (index >= 0 && index < _children.Count) + { + var child = _children[index]; + _children.RemoveAt(index); + child.Parent = null; + InvalidateMeasure(); + Invalidate(); + } + } - public override void OnPointerPressed(PointerEventArgs e) - { - SkiaView skiaView = HitTest(e.X, e.Y); - if (skiaView != null && skiaView != this) - { - skiaView.OnPointerPressed(e); - } - } + /// + /// Inserts a child at the specified index. + /// + public virtual void InsertChild(int index, SkiaView child) + { + if (child.Parent != null) + { + throw new InvalidOperationException("View already has a parent"); + } - public override void OnPointerReleased(PointerEventArgs e) - { - SkiaView skiaView = HitTest(e.X, e.Y); - if (skiaView != null && skiaView != this) - { - skiaView.OnPointerReleased(e); - } - } + _children.Insert(index, child); + child.Parent = this; - public override void OnPointerMoved(PointerEventArgs e) - { - SkiaView skiaView = HitTest(e.X, e.Y); - if (skiaView != null && skiaView != this) - { - skiaView.OnPointerMoved(e); - } - } + // Propagate binding context to new child + if (BindingContext != null) + { + SetInheritedBindingContext(child, BindingContext); + } - public override void OnScroll(ScrollEventArgs e) - { - SkiaView skiaView = HitTest(e.X, e.Y); - if (skiaView != null && skiaView != this) - { - skiaView.OnScroll(e); - } - } + InvalidateMeasure(); + Invalidate(); + } + + /// + /// Clears all children. + /// + public virtual void ClearChildren() + { + foreach (var child in _children) + { + child.Parent = null; + } + _children.Clear(); + InvalidateMeasure(); + Invalidate(); + } + + /// + /// Gets the content bounds (bounds minus padding). + /// + protected virtual SKRect GetContentBounds() + { + return GetContentBounds(Bounds); + } + + /// + /// Gets the content bounds for a given bounds rectangle. + /// + protected SKRect GetContentBounds(SKRect bounds) + { + return new SKRect( + bounds.Left + Padding.Left, + bounds.Top + Padding.Top, + bounds.Right - Padding.Right, + bounds.Bottom - Padding.Bottom); + } + + protected override void OnDraw(SKCanvas canvas, SKRect bounds) + { + // Draw background if set (for layouts inside CollectionView items) + if (BackgroundColor != SKColors.Transparent) + { + using var bgPaint = new SKPaint { Color = BackgroundColor, Style = SKPaintStyle.Fill }; + canvas.DrawRect(bounds, bgPaint); + } + + // Log for StackLayout + if (this is SkiaStackLayout) + { + bool hasCV = false; + foreach (var c in _children) + { + if (c is SkiaCollectionView) hasCV = true; + } + if (hasCV) + { + Console.WriteLine($"[SkiaStackLayout+CV] OnDraw - bounds={bounds}, children={_children.Count}"); + foreach (var c in _children) + { + Console.WriteLine($"[SkiaStackLayout+CV] Child: {c.GetType().Name}, IsVisible={c.IsVisible}, Bounds={c.Bounds}"); + } + } + } + + // Draw children in order + foreach (var child in _children) + { + if (child.IsVisible) + { + child.Draw(canvas); + } + } + } + + public override SkiaView? HitTest(float x, float y) + { + if (!IsVisible || !IsEnabled || !Bounds.Contains(new SKPoint(x, y))) + { + if (this is SkiaBorder) + { + Console.WriteLine($"[SkiaBorder.HitTest] Miss - x={x}, y={y}, Bounds={Bounds}, IsVisible={IsVisible}, IsEnabled={IsEnabled}"); + } + return null; + } + + // Hit test children in reverse order (top-most first) + for (int i = _children.Count - 1; i >= 0; i--) + { + var child = _children[i]; + var hit = child.HitTest(x, y); + if (hit != null) + { + if (this is SkiaBorder) + { + Console.WriteLine($"[SkiaBorder.HitTest] Hit child - x={x}, y={y}, Bounds={Bounds}, child={hit.GetType().Name}"); + } + return hit; + } + } + + if (this is SkiaBorder) + { + Console.WriteLine($"[SkiaBorder.HitTest] Hit self - x={x}, y={y}, Bounds={Bounds}, children={_children.Count}"); + } + return this; + } + + /// + /// Forward pointer pressed events to the appropriate child. + /// + public override void OnPointerPressed(PointerEventArgs e) + { + // Find which child was hit and forward the event + var hit = HitTest(e.X, e.Y); + if (hit != null && hit != this) + { + hit.OnPointerPressed(e); + } + } + + /// + /// Forward pointer released events to the appropriate child. + /// + public override void OnPointerReleased(PointerEventArgs e) + { + // Find which child was hit and forward the event + var hit = HitTest(e.X, e.Y); + if (hit != null && hit != this) + { + hit.OnPointerReleased(e); + } + } + + /// + /// Forward pointer moved events to the appropriate child. + /// + public override void OnPointerMoved(PointerEventArgs e) + { + // Find which child was hit and forward the event + var hit = HitTest(e.X, e.Y); + if (hit != null && hit != this) + { + hit.OnPointerMoved(e); + } + } + + /// + /// Forward scroll events to the appropriate child. + /// + public override void OnScroll(ScrollEventArgs e) + { + // Find which child was hit and forward the event + var hit = HitTest(e.X, e.Y); + if (hit != null && hit != this) + { + hit.OnScroll(e); + } + } +} + +/// +/// Stack layout that arranges children in a horizontal or vertical line. +/// +public class SkiaStackLayout : SkiaLayoutView +{ + /// + /// Bindable property for Orientation. + /// + public static readonly BindableProperty OrientationProperty = + BindableProperty.Create( + nameof(Orientation), + typeof(StackOrientation), + typeof(SkiaStackLayout), + StackOrientation.Vertical, + propertyChanged: (b, o, n) => ((SkiaStackLayout)b).InvalidateMeasure()); + + /// + /// Gets or sets the orientation of the stack. + /// + public StackOrientation Orientation + { + get => (StackOrientation)GetValue(OrientationProperty); + set => SetValue(OrientationProperty, value); + } + + protected override SKSize MeasureOverride(SKSize availableSize) + { + // Handle NaN/Infinity in padding + var paddingLeft = float.IsNaN(Padding.Left) ? 0 : Padding.Left; + var paddingRight = float.IsNaN(Padding.Right) ? 0 : Padding.Right; + var paddingTop = float.IsNaN(Padding.Top) ? 0 : Padding.Top; + var paddingBottom = float.IsNaN(Padding.Bottom) ? 0 : Padding.Bottom; + + var contentWidth = availableSize.Width - paddingLeft - paddingRight; + var contentHeight = availableSize.Height - paddingTop - paddingBottom; + + // Clamp negative sizes to 0 + if (contentWidth < 0 || float.IsNaN(contentWidth)) contentWidth = 0; + if (contentHeight < 0 || float.IsNaN(contentHeight)) contentHeight = 0; + + float totalWidth = 0; + float totalHeight = 0; + float maxWidth = 0; + float maxHeight = 0; + + var childAvailable = new SKSize(contentWidth, contentHeight); + + foreach (var child in Children) + { + if (!child.IsVisible) continue; + + var childSize = child.Measure(childAvailable); + + // Skip NaN sizes from child measurements + var childWidth = float.IsNaN(childSize.Width) ? 0 : childSize.Width; + var childHeight = float.IsNaN(childSize.Height) ? 0 : childSize.Height; + + if (Orientation == StackOrientation.Vertical) + { + totalHeight += childHeight; + maxWidth = Math.Max(maxWidth, childWidth); + } + else + { + totalWidth += childWidth; + maxHeight = Math.Max(maxHeight, childHeight); + } + } + + // Add spacing + var visibleCount = Children.Count(c => c.IsVisible); + var totalSpacing = Math.Max(0, visibleCount - 1) * Spacing; + + if (Orientation == StackOrientation.Vertical) + { + totalHeight += totalSpacing; + return new SKSize( + maxWidth + paddingLeft + paddingRight, + totalHeight + paddingTop + paddingBottom); + } + else + { + totalWidth += totalSpacing; + return new SKSize( + totalWidth + paddingLeft + paddingRight, + maxHeight + paddingTop + paddingBottom); + } + } + + protected override SKRect ArrangeOverride(SKRect bounds) + { + var content = GetContentBounds(bounds); + + // Clamp content dimensions if infinite - use reasonable defaults + var contentWidth = float.IsInfinity(content.Width) || float.IsNaN(content.Width) ? 800f : content.Width; + var contentHeight = float.IsInfinity(content.Height) || float.IsNaN(content.Height) ? 600f : content.Height; + + float offset = 0; + + foreach (var child in Children) + { + if (!child.IsVisible) continue; + + var childDesired = child.DesiredSize; + + // Handle NaN and Infinity in desired size + var childWidth = float.IsNaN(childDesired.Width) || float.IsInfinity(childDesired.Width) + ? contentWidth + : childDesired.Width; + var childHeight = float.IsNaN(childDesired.Height) || float.IsInfinity(childDesired.Height) + ? contentHeight + : childDesired.Height; + + SKRect childBounds; + if (Orientation == StackOrientation.Vertical) + { + // For ScrollView children, give them the remaining viewport height + // Clamp to avoid giving them their content size + var remainingHeight = Math.Max(0, contentHeight - offset); + var useHeight = child is SkiaScrollView + ? remainingHeight + : Math.Min(childHeight, remainingHeight > 0 ? remainingHeight : childHeight); + + childBounds = new SKRect( + content.Left, + content.Top + offset, + content.Left + contentWidth, + content.Top + offset + useHeight); + offset += useHeight + Spacing; + } + else + { + // For ScrollView children, give them the remaining viewport width + var remainingWidth = Math.Max(0, contentWidth - offset); + var useWidth = child is SkiaScrollView + ? remainingWidth + : Math.Min(childWidth, remainingWidth > 0 ? remainingWidth : childWidth); + + // Respect child's VerticalOptions for horizontal layouts + var useHeight = Math.Min(childHeight, contentHeight); + float childTop = content.Top; + float childBottom = content.Top + useHeight; + + var verticalOptions = child.VerticalOptions; + var alignmentValue = (int)verticalOptions.Alignment; + + // LayoutAlignment: Start=0, Center=1, End=2, Fill=3 + if (alignmentValue == 1) // Center + { + childTop = content.Top + (contentHeight - useHeight) / 2; + childBottom = childTop + useHeight; + } + else if (alignmentValue == 2) // End + { + childTop = content.Top + contentHeight - useHeight; + childBottom = content.Top + contentHeight; + } + else if (alignmentValue == 3) // Fill + { + childTop = content.Top; + childBottom = content.Top + contentHeight; + } + + childBounds = new SKRect( + content.Left + offset, + childTop, + content.Left + offset + useWidth, + childBottom); + offset += useWidth + Spacing; + } + + // Apply child's margin + var margin = child.Margin; + var marginedBounds = new SKRect( + childBounds.Left + (float)margin.Left, + childBounds.Top + (float)margin.Top, + childBounds.Right - (float)margin.Right, + childBounds.Bottom - (float)margin.Bottom); + child.Arrange(marginedBounds); + } + return bounds; + } +} + +/// +/// Stack orientation options. +/// +public enum StackOrientation +{ + Vertical, + Horizontal +} + +/// +/// Grid layout that arranges children in rows and columns. +/// +public class SkiaGrid : SkiaLayoutView +{ + #region BindableProperties + + /// + /// Bindable property for RowSpacing. + /// + public static readonly BindableProperty RowSpacingProperty = + BindableProperty.Create( + nameof(RowSpacing), + typeof(float), + typeof(SkiaGrid), + 0f, + propertyChanged: (b, o, n) => ((SkiaGrid)b).InvalidateMeasure()); + + /// + /// Bindable property for ColumnSpacing. + /// + public static readonly BindableProperty ColumnSpacingProperty = + BindableProperty.Create( + nameof(ColumnSpacing), + typeof(float), + typeof(SkiaGrid), + 0f, + propertyChanged: (b, o, n) => ((SkiaGrid)b).InvalidateMeasure()); + + #endregion + + private readonly List _rowDefinitions = new(); + private readonly List _columnDefinitions = new(); + private readonly Dictionary _childPositions = new(); + + private float[] _rowHeights = Array.Empty(); + private float[] _columnWidths = Array.Empty(); + + /// + /// Gets the row definitions. + /// + public IList RowDefinitions => _rowDefinitions; + + /// + /// Gets the column definitions. + /// + public IList ColumnDefinitions => _columnDefinitions; + + /// + /// Spacing between rows. + /// + public float RowSpacing + { + get => (float)GetValue(RowSpacingProperty); + set => SetValue(RowSpacingProperty, value); + } + + /// + /// Spacing between columns. + /// + public float ColumnSpacing + { + get => (float)GetValue(ColumnSpacingProperty); + set => SetValue(ColumnSpacingProperty, value); + } + + /// + /// Adds a child at the specified grid position. + /// + public void AddChild(SkiaView child, int row, int column, int rowSpan = 1, int columnSpan = 1) + { + base.AddChild(child); + _childPositions[child] = new GridPosition(row, column, rowSpan, columnSpan); + } + + public override void RemoveChild(SkiaView child) + { + base.RemoveChild(child); + _childPositions.Remove(child); + } + + /// + /// Gets the grid position of a child. + /// + public GridPosition GetPosition(SkiaView child) + { + return _childPositions.TryGetValue(child, out var pos) ? pos : new GridPosition(0, 0, 1, 1); + } + + /// + /// Sets the grid position of a child. + /// + public void SetPosition(SkiaView child, int row, int column, int rowSpan = 1, int columnSpan = 1) + { + _childPositions[child] = new GridPosition(row, column, rowSpan, columnSpan); + InvalidateMeasure(); + Invalidate(); + } + + protected override SKSize MeasureOverride(SKSize availableSize) + { + var contentWidth = availableSize.Width - Padding.Left - Padding.Right; + var contentHeight = availableSize.Height - Padding.Top - Padding.Bottom; + + // Handle NaN/Infinity + if (float.IsNaN(contentWidth) || float.IsInfinity(contentWidth)) contentWidth = 800; + if (float.IsNaN(contentHeight) || float.IsInfinity(contentHeight)) contentHeight = float.PositiveInfinity; + + var rowCount = Math.Max(1, _rowDefinitions.Count > 0 ? _rowDefinitions.Count : GetMaxRow() + 1); + var columnCount = Math.Max(1, _columnDefinitions.Count > 0 ? _columnDefinitions.Count : GetMaxColumn() + 1); + + // First pass: measure children in Auto columns to get natural widths + var columnNaturalWidths = new float[columnCount]; + var rowNaturalHeights = new float[rowCount]; + + foreach (var child in Children) + { + if (!child.IsVisible) continue; + + var pos = GetPosition(child); + + // For Auto columns, measure with infinite width to get natural size + var def = pos.Column < _columnDefinitions.Count ? _columnDefinitions[pos.Column] : GridLength.Star; + if (def.IsAuto && pos.ColumnSpan == 1) + { + var childSize = child.Measure(new SKSize(float.PositiveInfinity, float.PositiveInfinity)); + var childWidth = float.IsNaN(childSize.Width) ? 0 : childSize.Width; + columnNaturalWidths[pos.Column] = Math.Max(columnNaturalWidths[pos.Column], childWidth); + } + } + + // Calculate column widths - handle Auto, Absolute, and Star + _columnWidths = CalculateSizesWithAuto(_columnDefinitions, contentWidth, ColumnSpacing, columnCount, columnNaturalWidths); + + // Second pass: measure all children with calculated column widths + foreach (var child in Children) + { + if (!child.IsVisible) continue; + + var pos = GetPosition(child); + var cellWidth = GetCellWidth(pos.Column, pos.ColumnSpan); + + // Give infinite height for initial measure + var childSize = child.Measure(new SKSize(cellWidth, float.PositiveInfinity)); + + // Track max height for each row + // Cap infinite/very large heights - child returning infinity means it doesn't have a natural height + var childHeight = childSize.Height; + if (float.IsNaN(childHeight) || float.IsInfinity(childHeight) || childHeight > 100000) + { + // Use a default minimum - will be expanded by Star sizing if finite height is available + childHeight = 44; // Standard row height + } + if (pos.RowSpan == 1) + { + rowNaturalHeights[pos.Row] = Math.Max(rowNaturalHeights[pos.Row], childHeight); + } + } + + // Calculate row heights - use natural heights when available height is infinite or very large + // (Some layouts pass float.MaxValue instead of PositiveInfinity) + if (float.IsInfinity(contentHeight) || contentHeight > 100000) + { + _rowHeights = rowNaturalHeights; + } + else + { + _rowHeights = CalculateSizesWithAuto(_rowDefinitions, contentHeight, RowSpacing, rowCount, rowNaturalHeights); + } + + // Third pass: re-measure children with actual cell sizes + foreach (var child in Children) + { + if (!child.IsVisible) continue; + + var pos = GetPosition(child); + var cellWidth = GetCellWidth(pos.Column, pos.ColumnSpan); + var cellHeight = GetCellHeight(pos.Row, pos.RowSpan); + + child.Measure(new SKSize(cellWidth, cellHeight)); + } + + // Calculate total size + var totalWidth = _columnWidths.Sum() + Math.Max(0, columnCount - 1) * ColumnSpacing; + var totalHeight = _rowHeights.Sum() + Math.Max(0, rowCount - 1) * RowSpacing; + + return new SKSize( + totalWidth + Padding.Left + Padding.Right, + totalHeight + Padding.Top + Padding.Bottom); + } + + private int GetMaxRow() + { + int maxRow = 0; + foreach (var pos in _childPositions.Values) + { + maxRow = Math.Max(maxRow, pos.Row + pos.RowSpan - 1); + } + return maxRow; + } + + private int GetMaxColumn() + { + int maxCol = 0; + foreach (var pos in _childPositions.Values) + { + maxCol = Math.Max(maxCol, pos.Column + pos.ColumnSpan - 1); + } + return maxCol; + } + + private float[] CalculateSizesWithAuto(List definitions, float available, float spacing, int count, float[] naturalSizes) + { + if (count == 0) return new float[] { available }; + + var sizes = new float[count]; + var totalSpacing = Math.Max(0, count - 1) * spacing; + var remainingSpace = available - totalSpacing; + + // First pass: absolute and auto sizes + float starTotal = 0; + for (int i = 0; i < count; i++) + { + var def = i < definitions.Count ? definitions[i] : GridLength.Star; + + if (def.IsAbsolute) + { + sizes[i] = def.Value; + remainingSpace -= def.Value; + } + else if (def.IsAuto) + { + // Use natural size from measured children + sizes[i] = naturalSizes[i]; + remainingSpace -= sizes[i]; + } + else if (def.IsStar) + { + starTotal += def.Value; + } + } + + // Second pass: star sizes (distribute remaining space) + if (starTotal > 0 && remainingSpace > 0) + { + for (int i = 0; i < count; i++) + { + var def = i < definitions.Count ? definitions[i] : GridLength.Star; + if (def.IsStar) + { + sizes[i] = (def.Value / starTotal) * remainingSpace; + } + } + } + + return sizes; + } + + private float GetCellWidth(int column, int span) + { + float width = 0; + for (int i = column; i < Math.Min(column + span, _columnWidths.Length); i++) + { + width += _columnWidths[i]; + if (i > column) width += ColumnSpacing; + } + return width; + } + + private float GetCellHeight(int row, int span) + { + float height = 0; + for (int i = row; i < Math.Min(row + span, _rowHeights.Length); i++) + { + height += _rowHeights[i]; + if (i > row) height += RowSpacing; + } + return height; + } + + private float GetColumnOffset(int column) + { + float offset = 0; + for (int i = 0; i < Math.Min(column, _columnWidths.Length); i++) + { + offset += _columnWidths[i] + ColumnSpacing; + } + return offset; + } + + private float GetRowOffset(int row) + { + float offset = 0; + for (int i = 0; i < Math.Min(row, _rowHeights.Length); i++) + { + offset += _rowHeights[i] + RowSpacing; + } + return offset; + } + + protected override SKRect ArrangeOverride(SKRect bounds) + { + try + { + var content = GetContentBounds(bounds); + + // Recalculate row heights for arrange bounds if they differ from measurement + // This ensures Star rows expand to fill available space + var rowCount = _rowHeights.Length > 0 ? _rowHeights.Length : 1; + var columnCount = _columnWidths.Length > 0 ? _columnWidths.Length : 1; + var arrangeRowHeights = _rowHeights; + + // If we have arrange height and rows need recalculating + if (content.Height > 0 && !float.IsInfinity(content.Height)) + { + var measuredRowsTotal = _rowHeights.Sum() + Math.Max(0, rowCount - 1) * RowSpacing; + + // If arrange height is larger than measured, redistribute to Star rows + if (content.Height > measuredRowsTotal + 1) + { + arrangeRowHeights = new float[rowCount]; + var extraHeight = content.Height - measuredRowsTotal; + + // Count Star rows (implicit rows without definitions are Star) + float totalStarWeight = 0; + for (int i = 0; i < rowCount; i++) + { + var def = i < _rowDefinitions.Count ? _rowDefinitions[i] : GridLength.Star; + if (def.IsStar) totalStarWeight += def.Value; + } + + // Distribute extra height to Star rows + for (int i = 0; i < rowCount; i++) + { + var def = i < _rowDefinitions.Count ? _rowDefinitions[i] : GridLength.Star; + arrangeRowHeights[i] = i < _rowHeights.Length ? _rowHeights[i] : 0; + + if (def.IsStar && totalStarWeight > 0) + { + arrangeRowHeights[i] += extraHeight * (def.Value / totalStarWeight); + } + } + } + else + { + arrangeRowHeights = _rowHeights; + } + } + + foreach (var child in Children) + { + if (!child.IsVisible) continue; + + var pos = GetPosition(child); + + var x = content.Left + GetColumnOffset(pos.Column); + + // Calculate y using arrange row heights + float y = content.Top; + for (int i = 0; i < Math.Min(pos.Row, arrangeRowHeights.Length); i++) + { + y += arrangeRowHeights[i] + RowSpacing; + } + + var width = GetCellWidth(pos.Column, pos.ColumnSpan); + + // Calculate height using arrange row heights + float height = 0; + for (int i = pos.Row; i < Math.Min(pos.Row + pos.RowSpan, arrangeRowHeights.Length); i++) + { + height += arrangeRowHeights[i]; + if (i > pos.Row) height += RowSpacing; + } + + // Clamp infinite dimensions + if (float.IsInfinity(width) || float.IsNaN(width)) + width = content.Width; + if (float.IsInfinity(height) || float.IsNaN(height) || height <= 0) + height = content.Height; + + // Apply child's margin + var margin = child.Margin; + var marginedBounds = new SKRect( + x + (float)margin.Left, + y + (float)margin.Top, + x + width - (float)margin.Right, + y + height - (float)margin.Bottom); + child.Arrange(marginedBounds); + } + return bounds; + } + catch (Exception ex) + { + Console.WriteLine($"[SkiaGrid] EXCEPTION in ArrangeOverride: {ex.GetType().Name}: {ex.Message}"); + Console.WriteLine($"[SkiaGrid] Bounds: {bounds}, RowHeights: {_rowHeights.Length}, RowDefs: {_rowDefinitions.Count}, Children: {Children.Count}"); + Console.WriteLine($"[SkiaGrid] Stack trace: {ex.StackTrace}"); + throw; + } + } +} + +/// +/// Grid position information. +/// +public readonly struct GridPosition +{ + public int Row { get; } + public int Column { get; } + public int RowSpan { get; } + public int ColumnSpan { get; } + + public GridPosition(int row, int column, int rowSpan = 1, int columnSpan = 1) + { + Row = row; + Column = column; + RowSpan = Math.Max(1, rowSpan); + ColumnSpan = Math.Max(1, columnSpan); + } +} + +/// +/// Grid length specification. +/// +public readonly struct GridLength +{ + public float Value { get; } + public GridUnitType GridUnitType { get; } + + public bool IsAbsolute => GridUnitType == GridUnitType.Absolute; + public bool IsAuto => GridUnitType == GridUnitType.Auto; + public bool IsStar => GridUnitType == GridUnitType.Star; + + public static GridLength Auto => new(1, GridUnitType.Auto); + public static GridLength Star => new(1, GridUnitType.Star); + + public GridLength(float value, GridUnitType unitType = GridUnitType.Absolute) + { + Value = value; + GridUnitType = unitType; + } + + public static GridLength FromAbsolute(float value) => new(value, GridUnitType.Absolute); + public static GridLength FromStar(float value = 1) => new(value, GridUnitType.Star); +} + +/// +/// Grid unit type options. +/// +public enum GridUnitType +{ + Absolute, + Star, + Auto +} + +/// +/// Absolute layout that positions children at exact coordinates. +/// +public class SkiaAbsoluteLayout : SkiaLayoutView +{ + private readonly Dictionary _childBounds = new(); + + /// + /// Adds a child at the specified position and size. + /// + public void AddChild(SkiaView child, SKRect bounds, AbsoluteLayoutFlags flags = AbsoluteLayoutFlags.None) + { + base.AddChild(child); + _childBounds[child] = new AbsoluteLayoutBounds(bounds, flags); + } + + public override void RemoveChild(SkiaView child) + { + base.RemoveChild(child); + _childBounds.Remove(child); + } + + /// + /// Gets the layout bounds for a child. + /// + public AbsoluteLayoutBounds GetLayoutBounds(SkiaView child) + { + return _childBounds.TryGetValue(child, out var bounds) + ? bounds + : new AbsoluteLayoutBounds(SKRect.Empty, AbsoluteLayoutFlags.None); + } + + /// + /// Sets the layout bounds for a child. + /// + public void SetLayoutBounds(SkiaView child, SKRect bounds, AbsoluteLayoutFlags flags = AbsoluteLayoutFlags.None) + { + _childBounds[child] = new AbsoluteLayoutBounds(bounds, flags); + InvalidateMeasure(); + Invalidate(); + } + + protected override SKSize MeasureOverride(SKSize availableSize) + { + float maxRight = 0; + float maxBottom = 0; + + foreach (var child in Children) + { + if (!child.IsVisible) continue; + + var layout = GetLayoutBounds(child); + var bounds = layout.Bounds; + + child.Measure(new SKSize(bounds.Width, bounds.Height)); + + maxRight = Math.Max(maxRight, bounds.Right); + maxBottom = Math.Max(maxBottom, bounds.Bottom); + } + + return new SKSize( + maxRight + Padding.Left + Padding.Right, + maxBottom + Padding.Top + Padding.Bottom); + } + + protected override SKRect ArrangeOverride(SKRect bounds) + { + var content = GetContentBounds(bounds); + + foreach (var child in Children) + { + if (!child.IsVisible) continue; + + var layout = GetLayoutBounds(child); + var childBounds = layout.Bounds; + var flags = layout.Flags; + + float x, y, width, height; + + // X position + if (flags.HasFlag(AbsoluteLayoutFlags.XProportional)) + x = content.Left + childBounds.Left * content.Width; + else + x = content.Left + childBounds.Left; + + // Y position + if (flags.HasFlag(AbsoluteLayoutFlags.YProportional)) + y = content.Top + childBounds.Top * content.Height; + else + y = content.Top + childBounds.Top; + + // Width + if (flags.HasFlag(AbsoluteLayoutFlags.WidthProportional)) + width = childBounds.Width * content.Width; + else if (childBounds.Width < 0) + width = child.DesiredSize.Width; + else + width = childBounds.Width; + + // Height + if (flags.HasFlag(AbsoluteLayoutFlags.HeightProportional)) + height = childBounds.Height * content.Height; + else if (childBounds.Height < 0) + height = child.DesiredSize.Height; + else + height = childBounds.Height; + + // Apply child's margin + var margin = child.Margin; + var marginedBounds = new SKRect( + x + (float)margin.Left, + y + (float)margin.Top, + x + width - (float)margin.Right, + y + height - (float)margin.Bottom); + child.Arrange(marginedBounds); + } + return bounds; + } +} + +/// +/// Absolute layout bounds for a child. +/// +public readonly struct AbsoluteLayoutBounds +{ + public SKRect Bounds { get; } + public AbsoluteLayoutFlags Flags { get; } + + public AbsoluteLayoutBounds(SKRect bounds, AbsoluteLayoutFlags flags) + { + Bounds = bounds; + Flags = flags; + } +} + +/// +/// Flags for absolute layout positioning. +/// +[Flags] +public enum AbsoluteLayoutFlags +{ + None = 0, + XProportional = 1, + YProportional = 2, + WidthProportional = 4, + HeightProportional = 8, + PositionProportional = XProportional | YProportional, + SizeProportional = WidthProportional | HeightProportional, + All = XProportional | YProportional | WidthProportional | HeightProportional } diff --git a/Views/SkiaMenuBar.cs b/Views/SkiaMenuBar.cs index 3d698e8..d73816d 100644 --- a/Views/SkiaMenuBar.cs +++ b/Views/SkiaMenuBar.cs @@ -1,335 +1,598 @@ -using System; -using System.Collections.Generic; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using SkiaSharp; namespace Microsoft.Maui.Platform; +/// +/// A horizontal menu bar control. +/// public class SkiaMenuBar : SkiaView { - private readonly List _items = new List(); + private readonly List _items = new(); + private int _hoveredIndex = -1; + private int _openIndex = -1; + private SkiaMenuFlyout? _openFlyout; - private int _hoveredIndex = -1; + /// + /// Gets the menu bar items. + /// + public IList Items => _items; - private int _openIndex = -1; + /// + /// Gets or sets the background color. + /// + public SKColor BackgroundColor { get; set; } = new SKColor(240, 240, 240); - private SkiaMenuFlyout? _openFlyout; + /// + /// Gets or sets the text color. + /// + public SKColor TextColor { get; set; } = new SKColor(33, 33, 33); - public IList Items => _items; + /// + /// Gets or sets the hover background color. + /// + public SKColor HoverBackgroundColor { get; set; } = new SKColor(220, 220, 220); - public new SKColor BackgroundColor { get; set; } = new SKColor((byte)240, (byte)240, (byte)240); + /// + /// Gets or sets the active background color. + /// + public SKColor ActiveBackgroundColor { get; set; } = new SKColor(200, 200, 200); - public SKColor TextColor { get; set; } = new SKColor((byte)33, (byte)33, (byte)33); + /// + /// Gets or sets the bar height. + /// + public float BarHeight { get; set; } = 28f; - public SKColor HoverBackgroundColor { get; set; } = new SKColor((byte)220, (byte)220, (byte)220); + /// + /// Gets or sets the font size. + /// + public float FontSize { get; set; } = 13f; - public SKColor ActiveBackgroundColor { get; set; } = new SKColor((byte)200, (byte)200, (byte)200); + /// + /// Gets or sets the item padding. + /// + public float ItemPadding { get; set; } = 12f; - public float BarHeight { get; set; } = 28f; + protected override SKSize MeasureOverride(SKSize availableSize) + { + return new SKSize(availableSize.Width, BarHeight); + } - public float FontSize { get; set; } = 13f; + protected override void OnDraw(SKCanvas canvas, SKRect bounds) + { + canvas.Save(); - public float ItemPadding { get; set; } = 12f; + // Draw background + using var bgPaint = new SKPaint + { + Color = BackgroundColor, + Style = SKPaintStyle.Fill + }; + canvas.DrawRect(Bounds, bgPaint); - protected override SKSize MeasureOverride(SKSize availableSize) - { - //IL_000d: Unknown result type (might be due to invalid IL or missing references) - return new SKSize(((SKSize)(ref availableSize)).Width, BarHeight); - } + // Draw bottom border + using var borderPaint = new SKPaint + { + Color = new SKColor(200, 200, 200), + Style = SKPaintStyle.Stroke, + StrokeWidth = 1 + }; + canvas.DrawLine(Bounds.Left, Bounds.Bottom, Bounds.Right, Bounds.Bottom, borderPaint); - protected override void OnDraw(SKCanvas canvas, SKRect bounds) - { - //IL_0007: Unknown result type (might be due to invalid IL or missing references) - //IL_000c: Unknown result type (might be due to invalid IL or missing references) - //IL_000e: Unknown result type (might be due to invalid IL or missing references) - //IL_0018: Unknown result type (might be due to invalid IL or missing references) - //IL_0020: Expected O, but got Unknown - //IL_0022: Unknown result type (might be due to invalid IL or missing references) - //IL_002d: Unknown result type (might be due to invalid IL or missing references) - //IL_0032: Unknown result type (might be due to invalid IL or missing references) - //IL_0042: Unknown result type (might be due to invalid IL or missing references) - //IL_004c: Unknown result type (might be due to invalid IL or missing references) - //IL_0053: Unknown result type (might be due to invalid IL or missing references) - //IL_005f: Expected O, but got Unknown - //IL_0061: Unknown result type (might be due to invalid IL or missing references) - //IL_0066: Unknown result type (might be due to invalid IL or missing references) - //IL_0070: Unknown result type (might be due to invalid IL or missing references) - //IL_0075: Unknown result type (might be due to invalid IL or missing references) - //IL_007f: Unknown result type (might be due to invalid IL or missing references) - //IL_0084: Unknown result type (might be due to invalid IL or missing references) - //IL_008e: Unknown result type (might be due to invalid IL or missing references) - //IL_0093: Unknown result type (might be due to invalid IL or missing references) - //IL_00a2: Unknown result type (might be due to invalid IL or missing references) - //IL_00a7: Unknown result type (might be due to invalid IL or missing references) - //IL_00a9: Unknown result type (might be due to invalid IL or missing references) - //IL_00b3: Unknown result type (might be due to invalid IL or missing references) - //IL_00bf: Unknown result type (might be due to invalid IL or missing references) - //IL_00c7: Expected O, but got Unknown - //IL_00c8: Unknown result type (might be due to invalid IL or missing references) - //IL_00cd: Unknown result type (might be due to invalid IL or missing references) - //IL_00f0: Unknown result type (might be due to invalid IL or missing references) - //IL_0120: Unknown result type (might be due to invalid IL or missing references) - //IL_0125: Unknown result type (might be due to invalid IL or missing references) - //IL_0133: Unknown result type (might be due to invalid IL or missing references) - //IL_0138: Unknown result type (might be due to invalid IL or missing references) - //IL_0150: Unknown result type (might be due to invalid IL or missing references) - //IL_0155: Unknown result type (might be due to invalid IL or missing references) - //IL_0157: Unknown result type (might be due to invalid IL or missing references) - //IL_0161: Unknown result type (might be due to invalid IL or missing references) - //IL_016a: Expected O, but got Unknown - //IL_01c9: Unknown result type (might be due to invalid IL or missing references) - //IL_01ce: Unknown result type (might be due to invalid IL or missing references) - //IL_01f5: Unknown result type (might be due to invalid IL or missing references) - //IL_018c: Unknown result type (might be due to invalid IL or missing references) - //IL_0191: Unknown result type (might be due to invalid IL or missing references) - //IL_0193: Unknown result type (might be due to invalid IL or missing references) - //IL_019d: Unknown result type (might be due to invalid IL or missing references) - //IL_01a6: Expected O, but got Unknown - //IL_016b: Unknown result type (might be due to invalid IL or missing references) - //IL_01a7: Unknown result type (might be due to invalid IL or missing references) - canvas.Save(); - SKPaint val = new SKPaint - { - Color = BackgroundColor, - Style = (SKPaintStyle)0 - }; - try - { - canvas.DrawRect(base.Bounds, val); - SKPaint val2 = new SKPaint - { - Color = new SKColor((byte)200, (byte)200, (byte)200), - Style = (SKPaintStyle)1, - StrokeWidth = 1f - }; - try - { - SKRect bounds2 = base.Bounds; - float left = ((SKRect)(ref bounds2)).Left; - bounds2 = base.Bounds; - float bottom = ((SKRect)(ref bounds2)).Bottom; - bounds2 = base.Bounds; - float right = ((SKRect)(ref bounds2)).Right; - bounds2 = base.Bounds; - canvas.DrawLine(left, bottom, right, ((SKRect)(ref bounds2)).Bottom, val2); - SKPaint val3 = new SKPaint - { - Color = TextColor, - TextSize = FontSize, - IsAntialias = true - }; - try - { - bounds2 = base.Bounds; - float num = ((SKRect)(ref bounds2)).Left; - SKRect val5 = default(SKRect); - for (int i = 0; i < _items.Count; i++) - { - MenuBarItem menuBarItem = _items[i]; - SKRect val4 = default(SKRect); - val3.MeasureText(menuBarItem.Text, ref val4); - float num2 = ((SKRect)(ref val4)).Width + ItemPadding * 2f; - float num3 = num; - bounds2 = base.Bounds; - float top = ((SKRect)(ref bounds2)).Top; - float num4 = num + num2; - bounds2 = base.Bounds; - ((SKRect)(ref val5))._002Ector(num3, top, num4, ((SKRect)(ref bounds2)).Bottom); - if (i == _openIndex) - { - SKPaint val6 = new SKPaint - { - Color = ActiveBackgroundColor, - Style = (SKPaintStyle)0 - }; - try - { - canvas.DrawRect(val5, val6); - } - finally - { - ((IDisposable)val6)?.Dispose(); - } - } - else if (i == _hoveredIndex) - { - SKPaint val7 = new SKPaint - { - Color = HoverBackgroundColor, - Style = (SKPaintStyle)0 - }; - try - { - canvas.DrawRect(val5, val7); - } - finally - { - ((IDisposable)val7)?.Dispose(); - } - } - float num5 = num + ItemPadding; - bounds2 = base.Bounds; - float num6 = ((SKRect)(ref bounds2)).MidY - ((SKRect)(ref val4)).MidY; - canvas.DrawText(menuBarItem.Text, num5, num6, val3); - menuBarItem.Bounds = val5; - num += num2; - } - _openFlyout?.Draw(canvas); - canvas.Restore(); - } - finally - { - ((IDisposable)val3)?.Dispose(); - } - } - finally - { - ((IDisposable)val2)?.Dispose(); - } - } - finally - { - ((IDisposable)val)?.Dispose(); - } - } + // Draw menu items + using var textPaint = new SKPaint + { + Color = TextColor, + TextSize = FontSize, + IsAntialias = true + }; - public override SkiaView? HitTest(float x, float y) - { - //IL_0026: Unknown result type (might be due to invalid IL or missing references) - //IL_002b: Unknown result type (might be due to invalid IL or missing references) - if (!base.IsVisible) - { - return null; - } - if (_openFlyout != null) - { - SkiaView skiaView = _openFlyout.HitTest(x, y); - if (skiaView != null) - { - return skiaView; - } - } - SKRect bounds = base.Bounds; - if (((SKRect)(ref bounds)).Contains(x, y)) - { - return this; - } - if (_openFlyout != null) - { - CloseFlyout(); - } - return null; - } + float x = Bounds.Left; - public override void OnPointerMoved(PointerEventArgs e) - { - //IL_001b: Unknown result type (might be due to invalid IL or missing references) - //IL_0020: Unknown result type (might be due to invalid IL or missing references) - if (!base.IsEnabled) - { - return; - } - int num = -1; - for (int i = 0; i < _items.Count; i++) - { - SKRect bounds = _items[i].Bounds; - if (((SKRect)(ref bounds)).Contains(e.X, e.Y)) - { - num = i; - break; - } - } - if (num != _hoveredIndex) - { - _hoveredIndex = num; - if (_openIndex >= 0 && num >= 0 && num != _openIndex) - { - OpenFlyout(num); - } - Invalidate(); - } - base.OnPointerMoved(e); - } + for (int i = 0; i < _items.Count; i++) + { + var item = _items[i]; + var textBounds = new SKRect(); + textPaint.MeasureText(item.Text, ref textBounds); - public override void OnPointerPressed(PointerEventArgs e) - { - //IL_003c: Unknown result type (might be due to invalid IL or missing references) - //IL_0041: Unknown result type (might be due to invalid IL or missing references) - if (!base.IsEnabled) - { - return; - } - if (_openFlyout != null) - { - _openFlyout.OnPointerPressed(e); - if (e.Handled) - { - CloseFlyout(); - return; - } - } - for (int i = 0; i < _items.Count; i++) - { - SKRect bounds = _items[i].Bounds; - if (((SKRect)(ref bounds)).Contains(e.X, e.Y)) - { - if (_openIndex == i) - { - CloseFlyout(); - } - else - { - OpenFlyout(i); - } - e.Handled = true; - return; - } - } - if (_openFlyout != null) - { - CloseFlyout(); - e.Handled = true; - } - base.OnPointerPressed(e); - } + float itemWidth = textBounds.Width + ItemPadding * 2; + var itemBounds = new SKRect(x, Bounds.Top, x + itemWidth, Bounds.Bottom); - private void OpenFlyout(int index) - { - //IL_003f: Unknown result type (might be due to invalid IL or missing references) - //IL_0044: Unknown result type (might be due to invalid IL or missing references) - //IL_004e: Unknown result type (might be due to invalid IL or missing references) - //IL_0053: Unknown result type (might be due to invalid IL or missing references) - //IL_0064: Unknown result type (might be due to invalid IL or missing references) - if (index >= 0 && index < _items.Count) - { - MenuBarItem menuBarItem = _items[index]; - _openIndex = index; - _openFlyout = new SkiaMenuFlyout - { - Items = menuBarItem.Items - }; - SKRect bounds = menuBarItem.Bounds; - float left = ((SKRect)(ref bounds)).Left; - bounds = menuBarItem.Bounds; - float bottom = ((SKRect)(ref bounds)).Bottom; - _openFlyout.Position = new SKPoint(left, bottom); - _openFlyout.ItemClicked += OnFlyoutItemClicked; - Invalidate(); - } - } + // Draw item background + if (i == _openIndex) + { + using var activePaint = new SKPaint { Color = ActiveBackgroundColor, Style = SKPaintStyle.Fill }; + canvas.DrawRect(itemBounds, activePaint); + } + else if (i == _hoveredIndex) + { + using var hoverPaint = new SKPaint { Color = HoverBackgroundColor, Style = SKPaintStyle.Fill }; + canvas.DrawRect(itemBounds, hoverPaint); + } - private void CloseFlyout() - { - if (_openFlyout != null) - { - _openFlyout.ItemClicked -= OnFlyoutItemClicked; - _openFlyout = null; - } - _openIndex = -1; - Invalidate(); - } + // Draw text + float textX = x + ItemPadding; + float textY = Bounds.MidY - textBounds.MidY; + canvas.DrawText(item.Text, textX, textY, textPaint); - private void OnFlyoutItemClicked(object? sender, MenuItemClickedEventArgs e) - { - CloseFlyout(); - } + item.Bounds = itemBounds; + x += itemWidth; + } + + // Draw open flyout + _openFlyout?.Draw(canvas); + + canvas.Restore(); + } + + public override SkiaView? HitTest(float x, float y) + { + if (!IsVisible) return null; + + // Check flyout first + if (_openFlyout != null) + { + var flyoutHit = _openFlyout.HitTest(x, y); + if (flyoutHit != null) return flyoutHit; + } + + if (Bounds.Contains(x, y)) + { + return this; + } + + // Close flyout if clicking outside + if (_openFlyout != null) + { + CloseFlyout(); + } + + return null; + } + + public override void OnPointerMoved(PointerEventArgs e) + { + if (!IsEnabled) return; + + int newHovered = -1; + for (int i = 0; i < _items.Count; i++) + { + if (_items[i].Bounds.Contains(e.X, e.Y)) + { + newHovered = i; + break; + } + } + + if (newHovered != _hoveredIndex) + { + _hoveredIndex = newHovered; + + // If a menu is open and we hover another item, open that one + if (_openIndex >= 0 && newHovered >= 0 && newHovered != _openIndex) + { + OpenFlyout(newHovered); + } + + Invalidate(); + } + + base.OnPointerMoved(e); + } + + public override void OnPointerPressed(PointerEventArgs e) + { + if (!IsEnabled) return; + + // Check if clicking on flyout + if (_openFlyout != null) + { + _openFlyout.OnPointerPressed(e); + if (e.Handled) + { + CloseFlyout(); + return; + } + } + + // Check menu bar items + for (int i = 0; i < _items.Count; i++) + { + if (_items[i].Bounds.Contains(e.X, e.Y)) + { + if (_openIndex == i) + { + CloseFlyout(); + } + else + { + OpenFlyout(i); + } + e.Handled = true; + return; + } + } + + // Click outside - close flyout + if (_openFlyout != null) + { + CloseFlyout(); + e.Handled = true; + } + + base.OnPointerPressed(e); + } + + private void OpenFlyout(int index) + { + if (index < 0 || index >= _items.Count) return; + + var item = _items[index]; + _openIndex = index; + + _openFlyout = new SkiaMenuFlyout + { + Items = item.Items + }; + + // Position below the menu item + float x = item.Bounds.Left; + float y = item.Bounds.Bottom; + _openFlyout.Position = new SKPoint(x, y); + + _openFlyout.ItemClicked += OnFlyoutItemClicked; + Invalidate(); + } + + private void CloseFlyout() + { + if (_openFlyout != null) + { + _openFlyout.ItemClicked -= OnFlyoutItemClicked; + _openFlyout = null; + } + _openIndex = -1; + Invalidate(); + } + + private void OnFlyoutItemClicked(object? sender, MenuItemClickedEventArgs e) + { + CloseFlyout(); + } +} + +/// +/// Represents a top-level menu bar item. +/// +public class MenuBarItem +{ + /// + /// Gets or sets the display text. + /// + public string Text { get; set; } = string.Empty; + + /// + /// Gets the menu items. + /// + public List Items { get; } = new(); + + /// + /// Gets or sets the bounds (set during rendering). + /// + internal SKRect Bounds { get; set; } +} + +/// +/// Represents a menu item. +/// +public class MenuItem +{ + /// + /// Gets or sets the display text. + /// + public string Text { get; set; } = string.Empty; + + /// + /// Gets or sets the keyboard shortcut text. + /// + public string? Shortcut { get; set; } + + /// + /// Gets or sets whether this is a separator. + /// + public bool IsSeparator { get; set; } + + /// + /// Gets or sets whether this item is enabled. + /// + public bool IsEnabled { get; set; } = true; + + /// + /// Gets or sets whether this item is checked. + /// + public bool IsChecked { get; set; } + + /// + /// Gets or sets the icon source. + /// + public string? IconSource { get; set; } + + /// + /// Gets the sub-menu items. + /// + public List SubItems { get; } = new(); + + /// + /// Event raised when the item is clicked. + /// + public event EventHandler? Clicked; + + internal void OnClicked() + { + Clicked?.Invoke(this, EventArgs.Empty); + } +} + +/// +/// A dropdown menu flyout. +/// +public class SkiaMenuFlyout : SkiaView +{ + private int _hoveredIndex = -1; + private SKRect _bounds; + + /// + /// Gets or sets the menu items. + /// + public List Items { get; set; } = new(); + + /// + /// Gets or sets the position. + /// + public SKPoint Position { get; set; } + + /// + /// Gets or sets the background color. + /// + public SKColor BackgroundColor { get; set; } = SKColors.White; + + /// + /// Gets or sets the text color. + /// + public SKColor TextColor { get; set; } = new SKColor(33, 33, 33); + + /// + /// Gets or sets the disabled text color. + /// + public SKColor DisabledTextColor { get; set; } = new SKColor(160, 160, 160); + + /// + /// Gets or sets the hover background color. + /// + public SKColor HoverBackgroundColor { get; set; } = new SKColor(230, 230, 230); + + /// + /// Gets or sets the separator color. + /// + public SKColor SeparatorColor { get; set; } = new SKColor(220, 220, 220); + + /// + /// Gets or sets the font size. + /// + public float FontSize { get; set; } = 13f; + + /// + /// Gets or sets the item height. + /// + public float ItemHeight { get; set; } = 28f; + + /// + /// Gets or sets the separator height. + /// + public float SeparatorHeight { get; set; } = 9f; + + /// + /// Gets or sets the minimum width. + /// + public float MinWidth { get; set; } = 180f; + + /// + /// Event raised when an item is clicked. + /// + public event EventHandler? ItemClicked; + + protected override void OnDraw(SKCanvas canvas, SKRect bounds) + { + if (Items.Count == 0) return; + + // Calculate bounds + float width = MinWidth; + float height = 0; + + using var textPaint = new SKPaint + { + TextSize = FontSize, + IsAntialias = true + }; + + foreach (var item in Items) + { + if (item.IsSeparator) + { + height += SeparatorHeight; + } + else + { + height += ItemHeight; + + var textBounds = new SKRect(); + textPaint.MeasureText(item.Text, ref textBounds); + float itemWidth = textBounds.Width + 50; // Padding + icon space + if (!string.IsNullOrEmpty(item.Shortcut)) + { + textPaint.MeasureText(item.Shortcut, ref textBounds); + itemWidth += textBounds.Width + 20; + } + width = Math.Max(width, itemWidth); + } + } + + _bounds = new SKRect(Position.X, Position.Y, Position.X + width, Position.Y + height); + + // Draw shadow + using var shadowPaint = new SKPaint + { + ImageFilter = SKImageFilter.CreateDropShadow(0, 2, 8, 8, new SKColor(0, 0, 0, 40)) + }; + canvas.DrawRect(_bounds, shadowPaint); + + // Draw background + using var bgPaint = new SKPaint + { + Color = BackgroundColor, + Style = SKPaintStyle.Fill + }; + canvas.DrawRect(_bounds, bgPaint); + + // Draw border + using var borderPaint = new SKPaint + { + Color = new SKColor(200, 200, 200), + Style = SKPaintStyle.Stroke, + StrokeWidth = 1 + }; + canvas.DrawRect(_bounds, borderPaint); + + // Draw items + float y = _bounds.Top; + textPaint.Color = TextColor; + + for (int i = 0; i < Items.Count; i++) + { + var item = Items[i]; + + if (item.IsSeparator) + { + float separatorY = y + SeparatorHeight / 2; + using var sepPaint = new SKPaint { Color = SeparatorColor, StrokeWidth = 1 }; + canvas.DrawLine(_bounds.Left + 8, separatorY, _bounds.Right - 8, separatorY, sepPaint); + y += SeparatorHeight; + } + else + { + var itemBounds = new SKRect(_bounds.Left, y, _bounds.Right, y + ItemHeight); + + // Draw hover background + if (i == _hoveredIndex && item.IsEnabled) + { + using var hoverPaint = new SKPaint { Color = HoverBackgroundColor, Style = SKPaintStyle.Fill }; + canvas.DrawRect(itemBounds, hoverPaint); + } + + // Draw check mark + if (item.IsChecked) + { + using var checkPaint = new SKPaint + { + Color = item.IsEnabled ? TextColor : DisabledTextColor, + TextSize = FontSize, + IsAntialias = true + }; + canvas.DrawText("✓", _bounds.Left + 8, y + ItemHeight / 2 + 5, checkPaint); + } + + // Draw text + textPaint.Color = item.IsEnabled ? TextColor : DisabledTextColor; + canvas.DrawText(item.Text, _bounds.Left + 28, y + ItemHeight / 2 + 5, textPaint); + + // Draw shortcut + if (!string.IsNullOrEmpty(item.Shortcut)) + { + textPaint.Color = DisabledTextColor; + var shortcutBounds = new SKRect(); + textPaint.MeasureText(item.Shortcut, ref shortcutBounds); + canvas.DrawText(item.Shortcut, _bounds.Right - shortcutBounds.Width - 12, y + ItemHeight / 2 + 5, textPaint); + } + + // Draw submenu arrow + if (item.SubItems.Count > 0) + { + canvas.DrawText("▸", _bounds.Right - 16, y + ItemHeight / 2 + 5, textPaint); + } + + y += ItemHeight; + } + } + } + + public override SkiaView? HitTest(float x, float y) + { + if (_bounds.Contains(x, y)) + { + return this; + } + return null; + } + + public override void OnPointerMoved(PointerEventArgs e) + { + if (!_bounds.Contains(e.X, e.Y)) + { + _hoveredIndex = -1; + Invalidate(); + return; + } + + float y = _bounds.Top; + int newHovered = -1; + + for (int i = 0; i < Items.Count; i++) + { + var item = Items[i]; + float itemHeight = item.IsSeparator ? SeparatorHeight : ItemHeight; + + if (e.Y >= y && e.Y < y + itemHeight && !item.IsSeparator) + { + newHovered = i; + break; + } + + y += itemHeight; + } + + if (newHovered != _hoveredIndex) + { + _hoveredIndex = newHovered; + Invalidate(); + } + } + + public override void OnPointerPressed(PointerEventArgs e) + { + if (_hoveredIndex >= 0 && _hoveredIndex < Items.Count) + { + var item = Items[_hoveredIndex]; + if (item.IsEnabled && !item.IsSeparator) + { + item.OnClicked(); + ItemClicked?.Invoke(this, new MenuItemClickedEventArgs(item)); + e.Handled = true; + } + } + } +} + +/// +/// Event args for menu item clicked. +/// +public class MenuItemClickedEventArgs : EventArgs +{ + public MenuItem Item { get; } + + public MenuItemClickedEventArgs(MenuItem item) + { + Item = item; + } } diff --git a/Views/SkiaMenuFlyout.cs b/Views/SkiaMenuFlyout.cs deleted file mode 100644 index 93d9fce..0000000 --- a/Views/SkiaMenuFlyout.cs +++ /dev/null @@ -1,304 +0,0 @@ -using System; -using System.Collections.Generic; -using SkiaSharp; - -namespace Microsoft.Maui.Platform; - -public class SkiaMenuFlyout : SkiaView -{ - private int _hoveredIndex = -1; - - private SKRect _bounds; - - public List Items { get; set; } = new List(); - - public SKPoint Position { get; set; } - - public new SKColor BackgroundColor { get; set; } = SKColors.White; - - public SKColor TextColor { get; set; } = new SKColor((byte)33, (byte)33, (byte)33); - - public SKColor DisabledTextColor { get; set; } = new SKColor((byte)160, (byte)160, (byte)160); - - public SKColor HoverBackgroundColor { get; set; } = new SKColor((byte)230, (byte)230, (byte)230); - - public SKColor SeparatorColor { get; set; } = new SKColor((byte)220, (byte)220, (byte)220); - - public float FontSize { get; set; } = 13f; - - public float ItemHeight { get; set; } = 28f; - - public float SeparatorHeight { get; set; } = 9f; - - public float MinWidth { get; set; } = 180f; - - public event EventHandler? ItemClicked; - - protected override void OnDraw(SKCanvas canvas, SKRect bounds) - { - //IL_001b: Unknown result type (might be due to invalid IL or missing references) - //IL_0020: Unknown result type (might be due to invalid IL or missing references) - //IL_002c: Unknown result type (might be due to invalid IL or missing references) - //IL_0034: Expected O, but got Unknown - //IL_006e: Unknown result type (might be due to invalid IL or missing references) - //IL_00ea: Unknown result type (might be due to invalid IL or missing references) - //IL_00ef: Unknown result type (might be due to invalid IL or missing references) - //IL_00f9: Unknown result type (might be due to invalid IL or missing references) - //IL_00fe: Unknown result type (might be due to invalid IL or missing references) - //IL_0108: Unknown result type (might be due to invalid IL or missing references) - //IL_010d: Unknown result type (might be due to invalid IL or missing references) - //IL_0119: Unknown result type (might be due to invalid IL or missing references) - //IL_011e: Unknown result type (might be due to invalid IL or missing references) - //IL_0129: Unknown result type (might be due to invalid IL or missing references) - //IL_012e: Unknown result type (might be due to invalid IL or missing references) - //IL_0133: Unknown result type (might be due to invalid IL or missing references) - //IL_0138: Unknown result type (might be due to invalid IL or missing references) - //IL_0152: Unknown result type (might be due to invalid IL or missing references) - //IL_0162: Expected O, but got Unknown - //IL_0164: Unknown result type (might be due to invalid IL or missing references) - //IL_016f: Unknown result type (might be due to invalid IL or missing references) - //IL_0174: Unknown result type (might be due to invalid IL or missing references) - //IL_0176: Unknown result type (might be due to invalid IL or missing references) - //IL_0180: Unknown result type (might be due to invalid IL or missing references) - //IL_0189: Expected O, but got Unknown - //IL_018b: Unknown result type (might be due to invalid IL or missing references) - //IL_0197: Unknown result type (might be due to invalid IL or missing references) - //IL_019c: Unknown result type (might be due to invalid IL or missing references) - //IL_01ac: Unknown result type (might be due to invalid IL or missing references) - //IL_01b6: Unknown result type (might be due to invalid IL or missing references) - //IL_01bd: Unknown result type (might be due to invalid IL or missing references) - //IL_01ca: Expected O, but got Unknown - //IL_01cc: Unknown result type (might be due to invalid IL or missing references) - //IL_01e7: Unknown result type (might be due to invalid IL or missing references) - //IL_0222: Unknown result type (might be due to invalid IL or missing references) - //IL_0227: Unknown result type (might be due to invalid IL or missing references) - //IL_0229: Unknown result type (might be due to invalid IL or missing references) - //IL_0233: Unknown result type (might be due to invalid IL or missing references) - //IL_0240: Expected O, but got Unknown - //IL_0300: Unknown result type (might be due to invalid IL or missing references) - //IL_0305: Unknown result type (might be due to invalid IL or missing references) - //IL_02c5: Unknown result type (might be due to invalid IL or missing references) - //IL_02ca: Unknown result type (might be due to invalid IL or missing references) - //IL_02cc: Unknown result type (might be due to invalid IL or missing references) - //IL_02d6: Unknown result type (might be due to invalid IL or missing references) - //IL_02df: Expected O, but got Unknown - //IL_038b: Unknown result type (might be due to invalid IL or missing references) - //IL_0383: Unknown result type (might be due to invalid IL or missing references) - //IL_0318: Unknown result type (might be due to invalid IL or missing references) - //IL_0310: Unknown result type (might be due to invalid IL or missing references) - //IL_02e0: Unknown result type (might be due to invalid IL or missing references) - //IL_0322: Unknown result type (might be due to invalid IL or missing references) - //IL_032e: Unknown result type (might be due to invalid IL or missing references) - //IL_0337: Expected O, but got Unknown - //IL_03d9: Unknown result type (might be due to invalid IL or missing references) - //IL_03e5: Unknown result type (might be due to invalid IL or missing references) - if (Items.Count == 0) - { - return; - } - float num = MinWidth; - float num2 = 0f; - SKPaint val = new SKPaint - { - TextSize = FontSize, - IsAntialias = true - }; - try - { - foreach (MenuItem item in Items) - { - if (item.IsSeparator) - { - num2 += SeparatorHeight; - continue; - } - num2 += ItemHeight; - SKRect val2 = default(SKRect); - val.MeasureText(item.Text, ref val2); - float num3 = ((SKRect)(ref val2)).Width + 50f; - if (!string.IsNullOrEmpty(item.Shortcut)) - { - val.MeasureText(item.Shortcut, ref val2); - num3 += ((SKRect)(ref val2)).Width + 20f; - } - num = Math.Max(num, num3); - } - SKPoint position = Position; - float x = ((SKPoint)(ref position)).X; - position = Position; - float y = ((SKPoint)(ref position)).Y; - position = Position; - float num4 = ((SKPoint)(ref position)).X + num; - position = Position; - _bounds = new SKRect(x, y, num4, ((SKPoint)(ref position)).Y + num2); - SKPaint val3 = new SKPaint - { - ImageFilter = SKImageFilter.CreateDropShadow(0f, 2f, 8f, 8f, new SKColor((byte)0, (byte)0, (byte)0, (byte)40)) - }; - try - { - canvas.DrawRect(_bounds, val3); - SKPaint val4 = new SKPaint - { - Color = BackgroundColor, - Style = (SKPaintStyle)0 - }; - try - { - canvas.DrawRect(_bounds, val4); - SKPaint val5 = new SKPaint - { - Color = new SKColor((byte)200, (byte)200, (byte)200), - Style = (SKPaintStyle)1, - StrokeWidth = 1f - }; - try - { - canvas.DrawRect(_bounds, val5); - float num5 = ((SKRect)(ref _bounds)).Top; - val.Color = TextColor; - SKRect val7 = default(SKRect); - for (int i = 0; i < Items.Count; i++) - { - MenuItem menuItem = Items[i]; - if (menuItem.IsSeparator) - { - float num6 = num5 + SeparatorHeight / 2f; - SKPaint val6 = new SKPaint - { - Color = SeparatorColor, - StrokeWidth = 1f - }; - try - { - canvas.DrawLine(((SKRect)(ref _bounds)).Left + 8f, num6, ((SKRect)(ref _bounds)).Right - 8f, num6, val6); - num5 += SeparatorHeight; - } - finally - { - ((IDisposable)val6)?.Dispose(); - } - continue; - } - ((SKRect)(ref val7))._002Ector(((SKRect)(ref _bounds)).Left, num5, ((SKRect)(ref _bounds)).Right, num5 + ItemHeight); - if (i == _hoveredIndex && menuItem.IsEnabled) - { - SKPaint val8 = new SKPaint - { - Color = HoverBackgroundColor, - Style = (SKPaintStyle)0 - }; - try - { - canvas.DrawRect(val7, val8); - } - finally - { - ((IDisposable)val8)?.Dispose(); - } - } - if (menuItem.IsChecked) - { - SKPaint val9 = new SKPaint - { - Color = (menuItem.IsEnabled ? TextColor : DisabledTextColor), - TextSize = FontSize, - IsAntialias = true - }; - try - { - canvas.DrawText("✓", ((SKRect)(ref _bounds)).Left + 8f, num5 + ItemHeight / 2f + 5f, val9); - } - finally - { - ((IDisposable)val9)?.Dispose(); - } - } - val.Color = (menuItem.IsEnabled ? TextColor : DisabledTextColor); - canvas.DrawText(menuItem.Text, ((SKRect)(ref _bounds)).Left + 28f, num5 + ItemHeight / 2f + 5f, val); - if (!string.IsNullOrEmpty(menuItem.Shortcut)) - { - val.Color = DisabledTextColor; - SKRect val10 = default(SKRect); - val.MeasureText(menuItem.Shortcut, ref val10); - canvas.DrawText(menuItem.Shortcut, ((SKRect)(ref _bounds)).Right - ((SKRect)(ref val10)).Width - 12f, num5 + ItemHeight / 2f + 5f, val); - } - if (menuItem.SubItems.Count > 0) - { - canvas.DrawText("▸", ((SKRect)(ref _bounds)).Right - 16f, num5 + ItemHeight / 2f + 5f, val); - } - num5 += ItemHeight; - } - } - finally - { - ((IDisposable)val5)?.Dispose(); - } - } - finally - { - ((IDisposable)val4)?.Dispose(); - } - } - finally - { - ((IDisposable)val3)?.Dispose(); - } - } - finally - { - ((IDisposable)val)?.Dispose(); - } - } - - public override SkiaView? HitTest(float x, float y) - { - if (((SKRect)(ref _bounds)).Contains(x, y)) - { - return this; - } - return null; - } - - public override void OnPointerMoved(PointerEventArgs e) - { - if (!((SKRect)(ref _bounds)).Contains(e.X, e.Y)) - { - _hoveredIndex = -1; - Invalidate(); - return; - } - float num = ((SKRect)(ref _bounds)).Top; - int num2 = -1; - for (int i = 0; i < Items.Count; i++) - { - MenuItem menuItem = Items[i]; - float num3 = (menuItem.IsSeparator ? SeparatorHeight : ItemHeight); - if (e.Y >= num && e.Y < num + num3 && !menuItem.IsSeparator) - { - num2 = i; - break; - } - num += num3; - } - if (num2 != _hoveredIndex) - { - _hoveredIndex = num2; - Invalidate(); - } - } - - public override void OnPointerPressed(PointerEventArgs e) - { - if (_hoveredIndex >= 0 && _hoveredIndex < Items.Count) - { - MenuItem menuItem = Items[_hoveredIndex]; - if (menuItem.IsEnabled && !menuItem.IsSeparator) - { - menuItem.OnClicked(); - this.ItemClicked?.Invoke(this, new MenuItemClickedEventArgs(menuItem)); - e.Handled = true; - } - } - } -} diff --git a/Views/SkiaNavigationPage.cs b/Views/SkiaNavigationPage.cs index 36c06be..61c214e 100644 --- a/Views/SkiaNavigationPage.cs +++ b/Views/SkiaNavigationPage.cs @@ -1,513 +1,451 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.Maui.Platform.Linux; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using SkiaSharp; +using Microsoft.Maui.Graphics; namespace Microsoft.Maui.Platform; +/// +/// Skia-rendered navigation page with back stack support. +/// public class SkiaNavigationPage : SkiaView { - private readonly Stack _navigationStack = new Stack(); + private readonly Stack _navigationStack = new(); + private SkiaPage? _currentPage; + private bool _isAnimating; + private float _animationProgress; + private SkiaPage? _incomingPage; + private bool _isPushAnimation; - private SkiaPage? _currentPage; + // Navigation bar styling + private SKColor _barBackgroundColor = new SKColor(0x21, 0x96, 0xF3); + private SKColor _barTextColor = SKColors.White; + private float _navigationBarHeight = 56; + private bool _showBackButton = true; - private bool _isAnimating; + public SKColor BarBackgroundColor + { + get => _barBackgroundColor; + set + { + _barBackgroundColor = value; + UpdatePageNavigationBar(); + Invalidate(); + } + } - private float _animationProgress; + public SKColor BarTextColor + { + get => _barTextColor; + set + { + _barTextColor = value; + UpdatePageNavigationBar(); + Invalidate(); + } + } - private SkiaPage? _incomingPage; + public float NavigationBarHeight + { + get => _navigationBarHeight; + set + { + _navigationBarHeight = value; + UpdatePageNavigationBar(); + Invalidate(); + } + } - private bool _isPushAnimation; + public SkiaPage? CurrentPage => _currentPage; + public SkiaPage? RootPage => _navigationStack.Count > 0 ? _navigationStack.Last() : _currentPage; + public int StackDepth => _navigationStack.Count + (_currentPage != null ? 1 : 0); - private SKColor _barBackgroundColor = new SKColor((byte)33, (byte)150, (byte)243); + public event EventHandler? Pushed; + public event EventHandler? Popped; + public event EventHandler? PoppedToRoot; - private SKColor _barTextColor = SKColors.White; + public SkiaNavigationPage() + { + } - private float _navigationBarHeight = 56f; + public SkiaNavigationPage(SkiaPage rootPage) + { + SetRootPage(rootPage); + } - private bool _showBackButton = true; + public void SetRootPage(SkiaPage page) + { + _navigationStack.Clear(); + _currentPage?.OnDisappearing(); + _currentPage = page; + _currentPage.Parent = this; + ConfigurePage(_currentPage, false); + _currentPage.OnAppearing(); + Invalidate(); + } - public SKColor BarBackgroundColor - { - get - { - //IL_0001: Unknown result type (might be due to invalid IL or missing references) - return _barBackgroundColor; - } - set - { - //IL_0001: Unknown result type (might be due to invalid IL or missing references) - //IL_0002: Unknown result type (might be due to invalid IL or missing references) - _barBackgroundColor = value; - UpdatePageNavigationBar(); - Invalidate(); - } - } + public void Push(SkiaPage page, bool animated = true) + { + if (_isAnimating) return; - public SKColor BarTextColor - { - get - { - //IL_0001: Unknown result type (might be due to invalid IL or missing references) - return _barTextColor; - } - set - { - //IL_0001: Unknown result type (might be due to invalid IL or missing references) - //IL_0002: Unknown result type (might be due to invalid IL or missing references) - _barTextColor = value; - UpdatePageNavigationBar(); - Invalidate(); - } - } + if (_currentPage != null) + { + _currentPage.OnDisappearing(); + _navigationStack.Push(_currentPage); + } - public float NavigationBarHeight - { - get - { - return _navigationBarHeight; - } - set - { - _navigationBarHeight = value; - UpdatePageNavigationBar(); - Invalidate(); - } - } + ConfigurePage(page, true); + page.Parent = this; - public SkiaPage? CurrentPage => _currentPage; + if (animated) + { + _incomingPage = page; + _isPushAnimation = true; + _animationProgress = 0; + _isAnimating = true; + AnimatePush(); + } + else + { + _currentPage = page; + _currentPage.OnAppearing(); + Invalidate(); + } - public SkiaPage? RootPage - { - get - { - if (_navigationStack.Count <= 0) - { - return _currentPage; - } - return _navigationStack.Last(); - } - } + Pushed?.Invoke(this, new NavigationEventArgs(page)); + } - public int StackDepth => _navigationStack.Count + ((_currentPage != null) ? 1 : 0); + public SkiaPage? Pop(bool animated = true) + { + if (_isAnimating || _navigationStack.Count == 0) return null; - public event EventHandler? Pushed; + var poppedPage = _currentPage; + poppedPage?.OnDisappearing(); - public event EventHandler? Popped; + var previousPage = _navigationStack.Pop(); - public event EventHandler? PoppedToRoot; + if (animated && poppedPage != null) + { + _incomingPage = previousPage; + _isPushAnimation = false; + _animationProgress = 0; + _isAnimating = true; + AnimatePop(poppedPage); + } + else + { + _currentPage = previousPage; + _currentPage?.OnAppearing(); + Invalidate(); + } - public SkiaNavigationPage() - { - }//IL_0018: Unknown result type (might be due to invalid IL or missing references) - //IL_001d: Unknown result type (might be due to invalid IL or missing references) - //IL_0023: Unknown result type (might be due to invalid IL or missing references) - //IL_0028: Unknown result type (might be due to invalid IL or missing references) + if (poppedPage != null) + { + Popped?.Invoke(this, new NavigationEventArgs(poppedPage)); + } + return poppedPage; + } - public SkiaNavigationPage(SkiaPage rootPage) - { - //IL_0018: Unknown result type (might be due to invalid IL or missing references) - //IL_001d: Unknown result type (might be due to invalid IL or missing references) - //IL_0023: Unknown result type (might be due to invalid IL or missing references) - //IL_0028: Unknown result type (might be due to invalid IL or missing references) - SetRootPage(rootPage); - } + public void PopToRoot(bool animated = true) + { + if (_isAnimating || _navigationStack.Count == 0) return; - public void SetRootPage(SkiaPage page) - { - _navigationStack.Clear(); - _currentPage?.OnDisappearing(); - _currentPage = page; - _currentPage.Parent = this; - ConfigurePage(_currentPage, showBackButton: false); - _currentPage.OnAppearing(); - Invalidate(); - } + _currentPage?.OnDisappearing(); - public void Push(SkiaPage page, bool animated = true) - { - if (!_isAnimating) - { - if (LinuxApplication.IsGtkMode) - { - animated = false; - } - if (_currentPage != null) - { - _currentPage.OnDisappearing(); - _navigationStack.Push(_currentPage); - } - ConfigurePage(page, showBackButton: true); - page.Parent = this; - if (animated) - { - _incomingPage = page; - _isPushAnimation = true; - _animationProgress = 0f; - _isAnimating = true; - AnimatePush(); - } - else - { - Console.WriteLine("[SkiaNavigationPage] Push (no animation): setting _currentPage to " + page.Title); - _currentPage = page; - _currentPage.OnAppearing(); - Console.WriteLine("[SkiaNavigationPage] Push: calling Invalidate"); - Invalidate(); - Console.WriteLine("[SkiaNavigationPage] Push: Invalidate called, _currentPage is now " + _currentPage?.Title); - } - this.Pushed?.Invoke(this, new NavigationEventArgs(page)); - } - } + // Get root page + SkiaPage? rootPage = null; + while (_navigationStack.Count > 0) + { + rootPage = _navigationStack.Pop(); + } - public SkiaPage? Pop(bool animated = true) - { - if (_isAnimating || _navigationStack.Count == 0) - { - return null; - } - if (LinuxApplication.IsGtkMode) - { - animated = false; - } - SkiaPage currentPage = _currentPage; - currentPage?.OnDisappearing(); - SkiaPage skiaPage = _navigationStack.Pop(); - if (animated && currentPage != null) - { - _incomingPage = skiaPage; - _isPushAnimation = false; - _animationProgress = 0f; - _isAnimating = true; - AnimatePop(currentPage); - } - else - { - _currentPage = skiaPage; - _currentPage?.OnAppearing(); - Invalidate(); - } - if (currentPage != null) - { - this.Popped?.Invoke(this, new NavigationEventArgs(currentPage)); - } - return currentPage; - } + if (rootPage != null) + { + _currentPage = rootPage; + ConfigurePage(_currentPage, false); + _currentPage.OnAppearing(); + Invalidate(); + } - public void PopToRoot(bool animated = true) - { - if (!_isAnimating && _navigationStack.Count != 0) - { - _currentPage?.OnDisappearing(); - SkiaPage skiaPage = null; - while (_navigationStack.Count > 0) - { - skiaPage = _navigationStack.Pop(); - } - if (skiaPage != null) - { - _currentPage = skiaPage; - ConfigurePage(_currentPage, showBackButton: false); - _currentPage.OnAppearing(); - Invalidate(); - } - this.PoppedToRoot?.Invoke(this, new NavigationEventArgs(_currentPage)); - } - } + PoppedToRoot?.Invoke(this, new NavigationEventArgs(_currentPage!)); + } - private void ConfigurePage(SkiaPage page, bool showBackButton) - { - //IL_0009: Unknown result type (might be due to invalid IL or missing references) - //IL_0015: Unknown result type (might be due to invalid IL or missing references) - page.ShowNavigationBar = true; - page.TitleBarColor = _barBackgroundColor; - page.TitleTextColor = _barTextColor; - page.NavigationBarHeight = _navigationBarHeight; - _showBackButton = showBackButton && _navigationStack.Count > 0; - } + private void ConfigurePage(SkiaPage page, bool showBackButton) + { + page.ShowNavigationBar = true; + page.TitleBarColor = _barBackgroundColor; + page.TitleTextColor = _barTextColor; + page.NavigationBarHeight = _navigationBarHeight; + _showBackButton = showBackButton && _navigationStack.Count > 0; + } - private void UpdatePageNavigationBar() - { - //IL_000f: Unknown result type (might be due to invalid IL or missing references) - //IL_0020: Unknown result type (might be due to invalid IL or missing references) - if (_currentPage != null) - { - _currentPage.TitleBarColor = _barBackgroundColor; - _currentPage.TitleTextColor = _barTextColor; - _currentPage.NavigationBarHeight = _navigationBarHeight; - } - } + private void UpdatePageNavigationBar() + { + if (_currentPage != null) + { + _currentPage.TitleBarColor = _barBackgroundColor; + _currentPage.TitleTextColor = _barTextColor; + _currentPage.NavigationBarHeight = _navigationBarHeight; + } + } - private async void AnimatePush() - { - DateTime startTime = DateTime.Now; - while (_animationProgress < 1f) - { - await Task.Delay(16); - double totalMilliseconds = (DateTime.Now - startTime).TotalMilliseconds; - _animationProgress = Math.Min(1f, (float)(totalMilliseconds / 250.0)); - Invalidate(); - } - _currentPage = _incomingPage; - _incomingPage = null; - _isAnimating = false; - _currentPage?.OnAppearing(); - Invalidate(); - } + private async void AnimatePush() + { + const int durationMs = 250; + const int frameMs = 16; + var startTime = DateTime.Now; - private async void AnimatePop(SkiaPage outgoingPage) - { - DateTime startTime = DateTime.Now; - while (_animationProgress < 1f) - { - await Task.Delay(16); - double totalMilliseconds = (DateTime.Now - startTime).TotalMilliseconds; - _animationProgress = Math.Min(1f, (float)(totalMilliseconds / 250.0)); - Invalidate(); - } - _currentPage = _incomingPage; - _incomingPage = null; - _isAnimating = false; - _currentPage?.OnAppearing(); - outgoingPage.Parent = null; - Invalidate(); - } + while (_animationProgress < 1) + { + await Task.Delay(frameMs); + var elapsed = (DateTime.Now - startTime).TotalMilliseconds; + _animationProgress = Math.Min(1, (float)(elapsed / durationMs)); + Invalidate(); + } - protected override void OnDraw(SKCanvas canvas, SKRect bounds) - { - //IL_0001: Unknown result type (might be due to invalid IL or missing references) - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - //IL_0012: Unknown result type (might be due to invalid IL or missing references) - //IL_0017: Unknown result type (might be due to invalid IL or missing references) - //IL_0019: Unknown result type (might be due to invalid IL or missing references) - //IL_0023: Unknown result type (might be due to invalid IL or missing references) - //IL_002b: Expected O, but got Unknown - //IL_002c: Unknown result type (might be due to invalid IL or missing references) - //IL_01a7: Unknown result type (might be due to invalid IL or missing references) - //IL_0129: Unknown result type (might be due to invalid IL or missing references) - //IL_01d1: Unknown result type (might be due to invalid IL or missing references) - //IL_0166: Unknown result type (might be due to invalid IL or missing references) - //IL_00d9: Unknown result type (might be due to invalid IL or missing references) - //IL_00a8: Unknown result type (might be due to invalid IL or missing references) - if (base.BackgroundColor != SKColors.Transparent) - { - SKPaint val = new SKPaint - { - Color = base.BackgroundColor, - Style = (SKPaintStyle)0 - }; - try - { - canvas.DrawRect(bounds, val); - } - finally - { - ((IDisposable)val)?.Dispose(); - } - } - if (_isAnimating && _incomingPage != null) - { - float num = EaseOutCubic(_animationProgress); - if (_isPushAnimation) - { - float num2 = (0f - ((SKRect)(ref bounds)).Width) * num; - float num3 = ((SKRect)(ref bounds)).Width * (1f - num); - if (_currentPage != null) - { - canvas.Save(); - canvas.Translate(num2, 0f); - _currentPage.Bounds = bounds; - _currentPage.Draw(canvas); - canvas.Restore(); - } - canvas.Save(); - canvas.Translate(num3, 0f); - _incomingPage.Bounds = bounds; - _incomingPage.Draw(canvas); - canvas.Restore(); - } - else - { - float num4 = (0f - ((SKRect)(ref bounds)).Width) * (1f - num); - float num5 = ((SKRect)(ref bounds)).Width * num; - canvas.Save(); - canvas.Translate(num4, 0f); - _incomingPage.Bounds = bounds; - _incomingPage.Draw(canvas); - canvas.Restore(); - if (_currentPage != null) - { - canvas.Save(); - canvas.Translate(num5, 0f); - _currentPage.Bounds = bounds; - _currentPage.Draw(canvas); - canvas.Restore(); - } - } - } - else if (_currentPage != null) - { - Console.WriteLine("[SkiaNavigationPage] OnDraw: drawing _currentPage=" + _currentPage.Title); - _currentPage.Bounds = bounds; - _currentPage.Draw(canvas); - if (_showBackButton && _navigationStack.Count > 0) - { - DrawBackButton(canvas, bounds); - } - } - } + _currentPage = _incomingPage; + _incomingPage = null; + _isAnimating = false; + _currentPage?.OnAppearing(); + Invalidate(); + } - private void DrawBackButton(SKCanvas canvas, SKRect bounds) - { - //IL_0042: Unknown result type (might be due to invalid IL or missing references) - //IL_0047: Unknown result type (might be due to invalid IL or missing references) - //IL_0049: Unknown result type (might be due to invalid IL or missing references) - //IL_0053: Unknown result type (might be due to invalid IL or missing references) - //IL_005a: Unknown result type (might be due to invalid IL or missing references) - //IL_0065: Unknown result type (might be due to invalid IL or missing references) - //IL_006c: Unknown result type (might be due to invalid IL or missing references) - //IL_0074: Expected O, but got Unknown - //IL_0091: Unknown result type (might be due to invalid IL or missing references) - //IL_0098: Expected O, but got Unknown - SKRect val = default(SKRect); - ((SKRect)(ref val))._002Ector(((SKRect)(ref bounds)).Left + 8f, ((SKRect)(ref bounds)).Top + 12f, ((SKRect)(ref bounds)).Left + 48f, ((SKRect)(ref bounds)).Top + _navigationBarHeight - 12f); - SKPaint val2 = new SKPaint - { - Color = _barTextColor, - Style = (SKPaintStyle)1, - StrokeWidth = 2.5f, - IsAntialias = true, - StrokeCap = (SKStrokeCap)1 - }; - try - { - float midY = ((SKRect)(ref val)).MidY; - float num = 10f; - float num2 = ((SKRect)(ref val)).Left + 8f; - SKPath val3 = new SKPath(); - try - { - val3.MoveTo(num2 + num, midY - num); - val3.LineTo(num2, midY); - val3.LineTo(num2 + num, midY + num); - canvas.DrawPath(val3, val2); - } - finally - { - ((IDisposable)val3)?.Dispose(); - } - } - finally - { - ((IDisposable)val2)?.Dispose(); - } - } + private async void AnimatePop(SkiaPage outgoingPage) + { + const int durationMs = 250; + const int frameMs = 16; + var startTime = DateTime.Now; - private static float EaseOutCubic(float t) - { - return 1f - (float)Math.Pow(1f - t, 3.0); - } + while (_animationProgress < 1) + { + await Task.Delay(frameMs); + var elapsed = (DateTime.Now - startTime).TotalMilliseconds; + _animationProgress = Math.Min(1, (float)(elapsed / durationMs)); + Invalidate(); + } - protected override SKSize MeasureOverride(SKSize availableSize) - { - //IL_0000: Unknown result type (might be due to invalid IL or missing references) - return availableSize; - } + _currentPage = _incomingPage; + _incomingPage = null; + _isAnimating = false; + _currentPage?.OnAppearing(); + outgoingPage.Parent = null; + Invalidate(); + } - public override void OnPointerPressed(PointerEventArgs e) - { - Console.WriteLine($"[SkiaNavigationPage] OnPointerPressed at ({e.X}, {e.Y}), _isAnimating={_isAnimating}"); - if (!_isAnimating) - { - if (_showBackButton && _navigationStack.Count > 0 && e.X < 56f && e.Y < _navigationBarHeight) - { - Console.WriteLine("[SkiaNavigationPage] Back button clicked"); - Pop(); - } - else - { - Console.WriteLine("[SkiaNavigationPage] Forwarding to _currentPage: " + ((object)_currentPage)?.GetType().Name); - _currentPage?.OnPointerPressed(e); - } - } - } + protected override void OnDraw(SKCanvas canvas, SKRect bounds) + { + // Draw background + if (BackgroundColor != SKColors.Transparent) + { + using var bgPaint = new SKPaint + { + Color = BackgroundColor, + Style = SKPaintStyle.Fill + }; + canvas.DrawRect(bounds, bgPaint); + } - public override void OnPointerMoved(PointerEventArgs e) - { - if (!_isAnimating) - { - _currentPage?.OnPointerMoved(e); - } - } + if (_isAnimating && _incomingPage != null) + { + // Draw animation + var eased = EaseOutCubic(_animationProgress); - public override void OnPointerReleased(PointerEventArgs e) - { - if (!_isAnimating) - { - _currentPage?.OnPointerReleased(e); - } - } + if (_isPushAnimation) + { + // Push: current page slides left, incoming slides from right + var currentOffset = -bounds.Width * eased; + var incomingOffset = bounds.Width * (1 - eased); - public override void OnKeyDown(KeyEventArgs e) - { - if (!_isAnimating) - { - if ((e.Key == Key.Escape || e.Key == Key.Backspace) && _navigationStack.Count > 0) - { - Pop(); - e.Handled = true; - } - else - { - _currentPage?.OnKeyDown(e); - } - } - } + // Draw current page (sliding out) + if (_currentPage != null) + { + canvas.Save(); + canvas.Translate(currentOffset, 0); + _currentPage.Bounds = bounds; + _currentPage.Draw(canvas); + canvas.Restore(); + } - public override void OnKeyUp(KeyEventArgs e) - { - if (!_isAnimating) - { - _currentPage?.OnKeyUp(e); - } - } + // Draw incoming page + canvas.Save(); + canvas.Translate(incomingOffset, 0); + _incomingPage.Bounds = bounds; + _incomingPage.Draw(canvas); + canvas.Restore(); + } + else + { + // Pop: incoming slides from left, current slides right + var incomingOffset = -bounds.Width * (1 - eased); + var currentOffset = bounds.Width * eased; - public override void OnScroll(ScrollEventArgs e) - { - if (!_isAnimating) - { - _currentPage?.OnScroll(e); - } - } + // Draw incoming page (sliding in) + canvas.Save(); + canvas.Translate(incomingOffset, 0); + _incomingPage.Bounds = bounds; + _incomingPage.Draw(canvas); + canvas.Restore(); - public override SkiaView? HitTest(float x, float y) - { - if (!base.IsVisible) - { - return null; - } - if (_showBackButton && _navigationStack.Count > 0 && x < 56f && y < _navigationBarHeight) - { - return this; - } - if (_currentPage != null) - { - try - { - SkiaView skiaView = _currentPage.HitTest(x, y); - if (skiaView != null) - { - return skiaView; - } - } - catch (Exception ex) - { - Console.WriteLine("[SkiaNavigationPage] HitTest error: " + ex.Message); - } - } - return this; - } + // Draw current page (sliding out) + if (_currentPage != null) + { + canvas.Save(); + canvas.Translate(currentOffset, 0); + _currentPage.Bounds = bounds; + _currentPage.Draw(canvas); + canvas.Restore(); + } + } + } + else if (_currentPage != null) + { + // Draw current page normally + _currentPage.Bounds = bounds; + _currentPage.Draw(canvas); + + // Draw back button if applicable + if (_showBackButton && _navigationStack.Count > 0) + { + DrawBackButton(canvas, bounds); + } + } + } + + private void DrawBackButton(SKCanvas canvas, SKRect bounds) + { + var buttonBounds = new SKRect(bounds.Left + 8, bounds.Top + 12, bounds.Left + 48, bounds.Top + _navigationBarHeight - 12); + + using var paint = new SKPaint + { + Color = _barTextColor, + Style = SKPaintStyle.Stroke, + StrokeWidth = 2.5f, + IsAntialias = true, + StrokeCap = SKStrokeCap.Round + }; + + // Draw back arrow + var centerY = buttonBounds.MidY; + var arrowSize = 10f; + var left = buttonBounds.Left + 8; + + using var path = new SKPath(); + path.MoveTo(left + arrowSize, centerY - arrowSize); + path.LineTo(left, centerY); + path.LineTo(left + arrowSize, centerY + arrowSize); + canvas.DrawPath(path, paint); + } + + private static float EaseOutCubic(float t) + { + return 1 - (float)Math.Pow(1 - t, 3); + } + + protected override SKSize MeasureOverride(SKSize availableSize) + { + return availableSize; + } + + public override void OnPointerPressed(PointerEventArgs e) + { + Console.WriteLine($"[SkiaNavigationPage] OnPointerPressed at ({e.X}, {e.Y}), _isAnimating={_isAnimating}"); + if (_isAnimating) return; + + // Check for back button click + if (_showBackButton && _navigationStack.Count > 0) + { + if (e.X < 56 && e.Y < _navigationBarHeight) + { + Console.WriteLine($"[SkiaNavigationPage] Back button clicked"); + Pop(); + return; + } + } + + Console.WriteLine($"[SkiaNavigationPage] Forwarding to _currentPage: {_currentPage?.GetType().Name}"); + _currentPage?.OnPointerPressed(e); + } + + public override void OnPointerMoved(PointerEventArgs e) + { + if (_isAnimating) return; + _currentPage?.OnPointerMoved(e); + } + + public override void OnPointerReleased(PointerEventArgs e) + { + if (_isAnimating) return; + _currentPage?.OnPointerReleased(e); + } + + public override void OnKeyDown(KeyEventArgs e) + { + if (_isAnimating) return; + + // Handle back navigation with Escape or Backspace + if ((e.Key == Key.Escape || e.Key == Key.Backspace) && _navigationStack.Count > 0) + { + Pop(); + e.Handled = true; + return; + } + + _currentPage?.OnKeyDown(e); + } + + public override void OnKeyUp(KeyEventArgs e) + { + if (_isAnimating) return; + _currentPage?.OnKeyUp(e); + } + + public override void OnScroll(ScrollEventArgs e) + { + if (_isAnimating) return; + _currentPage?.OnScroll(e); + } + + public override SkiaView? HitTest(float x, float y) + { + if (!IsVisible) + return null; + + // Back button area - return self so OnPointerPressed handles it + if (_showBackButton && _navigationStack.Count > 0 && x < 56 && y < _navigationBarHeight) + { + return this; + } + + // Check current page + if (_currentPage != null) + { + try + { + var hit = _currentPage.HitTest(x, y); + if (hit != null) + return hit; + } + catch (Exception ex) + { + Console.WriteLine($"[SkiaNavigationPage] HitTest error: {ex.Message}"); + } + } + + return this; + } +} + +/// +/// Event args for navigation events. +/// +public class NavigationEventArgs : EventArgs +{ + public SkiaPage Page { get; } + + public NavigationEventArgs(SkiaPage page) + { + Page = page; + } } diff --git a/Views/SkiaPage.cs b/Views/SkiaPage.cs index 241e444..77bf6eb 100644 --- a/Views/SkiaPage.cs +++ b/Views/SkiaPage.cs @@ -1,458 +1,464 @@ -using System; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using SkiaSharp; +using Microsoft.Maui.Graphics; namespace Microsoft.Maui.Platform; +/// +/// Base class for Skia-rendered pages. +/// public class SkiaPage : SkiaView { - private SkiaView? _content; + private SkiaView? _content; + private string _title = ""; + private SKColor _titleBarColor = new SKColor(0x21, 0x96, 0xF3); // Material Blue + private SKColor _titleTextColor = SKColors.White; + private bool _showNavigationBar = false; + private float _navigationBarHeight = 56; - private string _title = ""; + // Padding + private float _paddingLeft; + private float _paddingTop; + private float _paddingRight; + private float _paddingBottom; - private SKColor _titleBarColor = new SKColor((byte)33, (byte)150, (byte)243); + public SkiaView? Content + { + get => _content; + set + { + if (_content != null) + { + _content.Parent = null; + } + _content = value; + if (_content != null) + { + _content.Parent = this; + } + Invalidate(); + } + } - private SKColor _titleTextColor = SKColors.White; + public string Title + { + get => _title; + set + { + _title = value; + Invalidate(); + } + } - private bool _showNavigationBar; + public SKColor TitleBarColor + { + get => _titleBarColor; + set + { + _titleBarColor = value; + Invalidate(); + } + } - private float _navigationBarHeight = 56f; + public SKColor TitleTextColor + { + get => _titleTextColor; + set + { + _titleTextColor = value; + Invalidate(); + } + } - private float _paddingLeft; + public bool ShowNavigationBar + { + get => _showNavigationBar; + set + { + _showNavigationBar = value; + Invalidate(); + } + } - private float _paddingTop; + public float NavigationBarHeight + { + get => _navigationBarHeight; + set + { + _navigationBarHeight = value; + Invalidate(); + } + } - private float _paddingRight; + public float PaddingLeft + { + get => _paddingLeft; + set { _paddingLeft = value; Invalidate(); } + } - private float _paddingBottom; + public float PaddingTop + { + get => _paddingTop; + set { _paddingTop = value; Invalidate(); } + } - public SkiaView? Content - { - get - { - return _content; - } - set - { - if (_content != null) - { - _content.Parent = null; - } - _content = value; - if (_content != null) - { - _content.Parent = this; - } - Invalidate(); - } - } + public float PaddingRight + { + get => _paddingRight; + set { _paddingRight = value; Invalidate(); } + } - public string Title - { - get - { - return _title; - } - set - { - _title = value; - Invalidate(); - } - } + public float PaddingBottom + { + get => _paddingBottom; + set { _paddingBottom = value; Invalidate(); } + } - public SKColor TitleBarColor - { - get - { - //IL_0001: Unknown result type (might be due to invalid IL or missing references) - return _titleBarColor; - } - set - { - //IL_0001: Unknown result type (might be due to invalid IL or missing references) - //IL_0002: Unknown result type (might be due to invalid IL or missing references) - _titleBarColor = value; - Invalidate(); - } - } + public bool IsBusy { get; set; } - public SKColor TitleTextColor - { - get - { - //IL_0001: Unknown result type (might be due to invalid IL or missing references) - return _titleTextColor; - } - set - { - //IL_0001: Unknown result type (might be due to invalid IL or missing references) - //IL_0002: Unknown result type (might be due to invalid IL or missing references) - _titleTextColor = value; - Invalidate(); - } - } + public event EventHandler? Appearing; + public event EventHandler? Disappearing; - public bool ShowNavigationBar - { - get - { - return _showNavigationBar; - } - set - { - _showNavigationBar = value; - Invalidate(); - } - } + protected override void OnDraw(SKCanvas canvas, SKRect bounds) + { + // Draw background + if (BackgroundColor != SKColors.Transparent) + { + using var bgPaint = new SKPaint + { + Color = BackgroundColor, + Style = SKPaintStyle.Fill + }; + canvas.DrawRect(bounds, bgPaint); + } - public float NavigationBarHeight - { - get - { - return _navigationBarHeight; - } - set - { - _navigationBarHeight = value; - Invalidate(); - } - } + var contentTop = bounds.Top; - public float PaddingLeft - { - get - { - return _paddingLeft; - } - set - { - _paddingLeft = value; - Invalidate(); - } - } + // Draw navigation bar if visible + if (_showNavigationBar) + { + DrawNavigationBar(canvas, new SKRect(bounds.Left, bounds.Top, bounds.Right, bounds.Top + _navigationBarHeight)); + contentTop = bounds.Top + _navigationBarHeight; + } - public float PaddingTop - { - get - { - return _paddingTop; - } - set - { - _paddingTop = value; - Invalidate(); - } - } + // Calculate content bounds with padding + var contentBounds = new SKRect( + bounds.Left + _paddingLeft, + contentTop + _paddingTop, + bounds.Right - _paddingRight, + bounds.Bottom - _paddingBottom); - public float PaddingRight - { - get - { - return _paddingRight; - } - set - { - _paddingRight = value; - Invalidate(); - } - } + // Draw content + if (_content != null) + { + // Apply content's margin to the content bounds + var margin = _content.Margin; + var adjustedBounds = new SKRect( + contentBounds.Left + (float)margin.Left, + contentBounds.Top + (float)margin.Top, + contentBounds.Right - (float)margin.Right, + contentBounds.Bottom - (float)margin.Bottom); - public float PaddingBottom - { - get - { - return _paddingBottom; - } - set - { - _paddingBottom = value; - Invalidate(); - } - } + // Measure and arrange the content before drawing + var availableSize = new SKSize(adjustedBounds.Width, adjustedBounds.Height); + _content.Measure(availableSize); + _content.Arrange(adjustedBounds); + Console.WriteLine($"[SkiaPage] Drawing content: {_content.GetType().Name}, Bounds={_content.Bounds}, IsVisible={_content.IsVisible}"); + _content.Draw(canvas); + } - public bool IsBusy { get; set; } + // Draw busy indicator overlay + if (IsBusy) + { + DrawBusyIndicator(canvas, bounds); + } + } - public event EventHandler? Appearing; + protected virtual void DrawNavigationBar(SKCanvas canvas, SKRect bounds) + { + // Draw navigation bar background + using var barPaint = new SKPaint + { + Color = _titleBarColor, + Style = SKPaintStyle.Fill + }; + canvas.DrawRect(bounds, barPaint); - public event EventHandler? Disappearing; + // Draw title + if (!string.IsNullOrEmpty(_title)) + { + using var font = new SKFont(SKTypeface.Default, 20); + using var textPaint = new SKPaint(font) + { + Color = _titleTextColor, + IsAntialias = true + }; - protected override void OnDraw(SKCanvas canvas, SKRect bounds) - { - //IL_0001: Unknown result type (might be due to invalid IL or missing references) - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - //IL_0012: Unknown result type (might be due to invalid IL or missing references) - //IL_0017: Unknown result type (might be due to invalid IL or missing references) - //IL_0019: Unknown result type (might be due to invalid IL or missing references) - //IL_0023: Unknown result type (might be due to invalid IL or missing references) - //IL_002b: Expected O, but got Unknown - //IL_0074: Unknown result type (might be due to invalid IL or missing references) - //IL_002c: Unknown result type (might be due to invalid IL or missing references) - //IL_00d7: Unknown result type (might be due to invalid IL or missing references) - //IL_00dc: Unknown result type (might be due to invalid IL or missing references) - //IL_013f: Unknown result type (might be due to invalid IL or missing references) - //IL_0141: Unknown result type (might be due to invalid IL or missing references) - //IL_014d: Unknown result type (might be due to invalid IL or missing references) - //IL_0195: Unknown result type (might be due to invalid IL or missing references) - //IL_01df: Unknown result type (might be due to invalid IL or missing references) - if (base.BackgroundColor != SKColors.Transparent) - { - SKPaint val = new SKPaint - { - Color = base.BackgroundColor, - Style = (SKPaintStyle)0 - }; - try - { - canvas.DrawRect(bounds, val); - } - finally - { - ((IDisposable)val)?.Dispose(); - } - } - float num = ((SKRect)(ref bounds)).Top; - if (_showNavigationBar) - { - DrawNavigationBar(canvas, new SKRect(((SKRect)(ref bounds)).Left, ((SKRect)(ref bounds)).Top, ((SKRect)(ref bounds)).Right, ((SKRect)(ref bounds)).Top + _navigationBarHeight)); - num = ((SKRect)(ref bounds)).Top + _navigationBarHeight; - } - SKRect val2 = default(SKRect); - ((SKRect)(ref val2))._002Ector(((SKRect)(ref bounds)).Left + _paddingLeft, num + _paddingTop, ((SKRect)(ref bounds)).Right - _paddingRight, ((SKRect)(ref bounds)).Bottom - _paddingBottom); - if (_content != null) - { - Thickness margin = _content.Margin; - SKRect bounds2 = default(SKRect); - ((SKRect)(ref bounds2))._002Ector(((SKRect)(ref val2)).Left + (float)((Thickness)(ref margin)).Left, ((SKRect)(ref val2)).Top + (float)((Thickness)(ref margin)).Top, ((SKRect)(ref val2)).Right - (float)((Thickness)(ref margin)).Right, ((SKRect)(ref val2)).Bottom - (float)((Thickness)(ref margin)).Bottom); - SKSize availableSize = default(SKSize); - ((SKSize)(ref availableSize))._002Ector(((SKRect)(ref bounds2)).Width, ((SKRect)(ref bounds2)).Height); - _content.Measure(availableSize); - _content.Arrange(bounds2); - Console.WriteLine($"[SkiaPage] Drawing content: {((object)_content).GetType().Name}, Bounds={_content.Bounds}, IsVisible={_content.IsVisible}"); - _content.Draw(canvas); - } - if (IsBusy) - { - DrawBusyIndicator(canvas, bounds); - } - } + var textBounds = new SKRect(); + textPaint.MeasureText(_title, ref textBounds); - protected virtual void DrawNavigationBar(SKCanvas canvas, SKRect bounds) - { - //IL_0000: Unknown result type (might be due to invalid IL or missing references) - //IL_0005: Unknown result type (might be due to invalid IL or missing references) - //IL_0007: Unknown result type (might be due to invalid IL or missing references) - //IL_0011: Unknown result type (might be due to invalid IL or missing references) - //IL_0019: Expected O, but got Unknown - //IL_001a: Unknown result type (might be due to invalid IL or missing references) - //IL_00c3: Unknown result type (might be due to invalid IL or missing references) - //IL_00c8: Unknown result type (might be due to invalid IL or missing references) - //IL_00ce: Unknown result type (might be due to invalid IL or missing references) - //IL_00d8: Unknown result type (might be due to invalid IL or missing references) - //IL_00df: Unknown result type (might be due to invalid IL or missing references) - //IL_00f1: Expected O, but got Unknown - //IL_0045: Unknown result type (might be due to invalid IL or missing references) - //IL_004b: Expected O, but got Unknown - //IL_0114: Unknown result type (might be due to invalid IL or missing references) - //IL_004c: Unknown result type (might be due to invalid IL or missing references) - //IL_0051: Unknown result type (might be due to invalid IL or missing references) - //IL_0053: Unknown result type (might be due to invalid IL or missing references) - //IL_005d: Unknown result type (might be due to invalid IL or missing references) - //IL_0065: Expected O, but got Unknown - //IL_0067: Unknown result type (might be due to invalid IL or missing references) - SKPaint val = new SKPaint - { - Color = _titleBarColor, - Style = (SKPaintStyle)0 - }; - try - { - canvas.DrawRect(bounds, val); - if (!string.IsNullOrEmpty(_title)) - { - SKFont val2 = new SKFont(SKTypeface.Default, 20f, 1f, 0f); - try - { - SKPaint val3 = new SKPaint(val2) - { - Color = _titleTextColor, - IsAntialias = true - }; - try - { - SKRect val4 = default(SKRect); - val3.MeasureText(_title, ref val4); - float num = ((SKRect)(ref bounds)).Left + 16f; - float num2 = ((SKRect)(ref bounds)).MidY - ((SKRect)(ref val4)).MidY; - canvas.DrawText(_title, num, num2, val3); - } - finally - { - ((IDisposable)val3)?.Dispose(); - } - } - finally - { - ((IDisposable)val2)?.Dispose(); - } - } - SKPaint val5 = new SKPaint - { - Color = new SKColor((byte)0, (byte)0, (byte)0, (byte)30), - Style = (SKPaintStyle)0, - MaskFilter = SKMaskFilter.CreateBlur((SKBlurStyle)0, 2f) - }; - try - { - canvas.DrawRect(new SKRect(((SKRect)(ref bounds)).Left, ((SKRect)(ref bounds)).Bottom, ((SKRect)(ref bounds)).Right, ((SKRect)(ref bounds)).Bottom + 4f), val5); - } - finally - { - ((IDisposable)val5)?.Dispose(); - } - } - finally - { - ((IDisposable)val)?.Dispose(); - } - } + var x = bounds.Left + 16; + var y = bounds.MidY - textBounds.MidY; + canvas.DrawText(_title, x, y, textPaint); + } - private void DrawBusyIndicator(SKCanvas canvas, SKRect bounds) - { - //IL_0000: Unknown result type (might be due to invalid IL or missing references) - //IL_0005: Unknown result type (might be due to invalid IL or missing references) - //IL_001a: Unknown result type (might be due to invalid IL or missing references) - //IL_0024: Unknown result type (might be due to invalid IL or missing references) - //IL_002c: Expected O, but got Unknown - //IL_002d: Unknown result type (might be due to invalid IL or missing references) - //IL_0034: Unknown result type (might be due to invalid IL or missing references) - //IL_0039: Unknown result type (might be due to invalid IL or missing references) - //IL_003b: Unknown result type (might be due to invalid IL or missing references) - //IL_0045: Unknown result type (might be due to invalid IL or missing references) - //IL_004c: Unknown result type (might be due to invalid IL or missing references) - //IL_0057: Unknown result type (might be due to invalid IL or missing references) - //IL_005e: Unknown result type (might be due to invalid IL or missing references) - //IL_0066: Expected O, but got Unknown - //IL_007d: Unknown result type (might be due to invalid IL or missing references) - //IL_0084: Expected O, but got Unknown - //IL_0096: Unknown result type (might be due to invalid IL or missing references) - SKPaint val = new SKPaint - { - Color = new SKColor(byte.MaxValue, byte.MaxValue, byte.MaxValue, (byte)180), - Style = (SKPaintStyle)0 - }; - try - { - canvas.DrawRect(bounds, val); - SKPaint val2 = new SKPaint - { - Color = _titleBarColor, - Style = (SKPaintStyle)1, - StrokeWidth = 4f, - IsAntialias = true, - StrokeCap = (SKStrokeCap)1 - }; - try - { - float midX = ((SKRect)(ref bounds)).MidX; - float midY = ((SKRect)(ref bounds)).MidY; - float num = 20f; - SKPath val3 = new SKPath(); - try - { - val3.AddArc(new SKRect(midX - num, midY - num, midX + num, midY + num), 0f, 270f); - canvas.DrawPath(val3, val2); - } - finally - { - ((IDisposable)val3)?.Dispose(); - } - } - finally - { - ((IDisposable)val2)?.Dispose(); - } - } - finally - { - ((IDisposable)val)?.Dispose(); - } - } + // Draw shadow + using var shadowPaint = new SKPaint + { + Color = new SKColor(0, 0, 0, 30), + Style = SKPaintStyle.Fill, + MaskFilter = SKMaskFilter.CreateBlur(SKBlurStyle.Normal, 2) + }; + canvas.DrawRect(new SKRect(bounds.Left, bounds.Bottom, bounds.Right, bounds.Bottom + 4), shadowPaint); + } - public void OnAppearing() - { - Console.WriteLine($"[SkiaPage] OnAppearing called for: {Title}, HasListeners={this.Appearing != null}"); - this.Appearing?.Invoke(this, EventArgs.Empty); - } + private void DrawBusyIndicator(SKCanvas canvas, SKRect bounds) + { + // Draw semi-transparent overlay + using var overlayPaint = new SKPaint + { + Color = new SKColor(255, 255, 255, 180), + Style = SKPaintStyle.Fill + }; + canvas.DrawRect(bounds, overlayPaint); - public void OnDisappearing() - { - this.Disappearing?.Invoke(this, EventArgs.Empty); - } + // Draw spinning indicator (simplified - would animate in real impl) + using var indicatorPaint = new SKPaint + { + Color = _titleBarColor, + Style = SKPaintStyle.Stroke, + StrokeWidth = 4, + IsAntialias = true, + StrokeCap = SKStrokeCap.Round + }; - protected override SKSize MeasureOverride(SKSize availableSize) - { - //IL_0000: Unknown result type (might be due to invalid IL or missing references) - return availableSize; - } + var centerX = bounds.MidX; + var centerY = bounds.MidY; + var radius = 20f; - public override void OnPointerPressed(PointerEventArgs e) - { - float num = (_showNavigationBar ? _navigationBarHeight : 0f); - if (e.Y > num && _content != null) - { - PointerEventArgs e2 = new PointerEventArgs(e.X - _paddingLeft, e.Y - num - _paddingTop, e.Button); - _content.OnPointerPressed(e2); - } - } + using var path = new SKPath(); + path.AddArc(new SKRect(centerX - radius, centerY - radius, centerX + radius, centerY + radius), 0, 270); + canvas.DrawPath(path, indicatorPaint); + } - public override void OnPointerMoved(PointerEventArgs e) - { - float num = (_showNavigationBar ? _navigationBarHeight : 0f); - if (e.Y > num && _content != null) - { - PointerEventArgs e2 = new PointerEventArgs(e.X - _paddingLeft, e.Y - num - _paddingTop, e.Button); - _content.OnPointerMoved(e2); - } - } + public void OnAppearing() + { + Console.WriteLine($"[SkiaPage] OnAppearing called for: {Title}, HasListeners={Appearing != null}"); + Appearing?.Invoke(this, EventArgs.Empty); + } - public override void OnPointerReleased(PointerEventArgs e) - { - float num = (_showNavigationBar ? _navigationBarHeight : 0f); - if (e.Y > num && _content != null) - { - PointerEventArgs e2 = new PointerEventArgs(e.X - _paddingLeft, e.Y - num - _paddingTop, e.Button); - _content.OnPointerReleased(e2); - } - } + public void OnDisappearing() + { + Disappearing?.Invoke(this, EventArgs.Empty); + } - public override void OnKeyDown(KeyEventArgs e) - { - _content?.OnKeyDown(e); - } + protected override SKSize MeasureOverride(SKSize availableSize) + { + // Page takes all available space + return availableSize; + } - public override void OnKeyUp(KeyEventArgs e) - { - _content?.OnKeyUp(e); - } + public override void OnPointerPressed(PointerEventArgs e) + { + // Adjust coordinates for content + var contentTop = _showNavigationBar ? _navigationBarHeight : 0; + if (e.Y > contentTop && _content != null) + { + var contentE = new PointerEventArgs(e.X - _paddingLeft, e.Y - contentTop - _paddingTop, e.Button); + _content.OnPointerPressed(contentE); + } + } - public override void OnScroll(ScrollEventArgs e) - { - _content?.OnScroll(e); - } + public override void OnPointerMoved(PointerEventArgs e) + { + var contentTop = _showNavigationBar ? _navigationBarHeight : 0; + if (e.Y > contentTop && _content != null) + { + var contentE = new PointerEventArgs(e.X - _paddingLeft, e.Y - contentTop - _paddingTop, e.Button); + _content.OnPointerMoved(contentE); + } + } - public override SkiaView? HitTest(float x, float y) - { - if (!base.IsVisible) - { - return null; - } - if (_content != null) - { - SkiaView skiaView = _content.HitTest(x, y); - if (skiaView != null) - { - return skiaView; - } - } - return this; - } + public override void OnPointerReleased(PointerEventArgs e) + { + var contentTop = _showNavigationBar ? _navigationBarHeight : 0; + if (e.Y > contentTop && _content != null) + { + var contentE = new PointerEventArgs(e.X - _paddingLeft, e.Y - contentTop - _paddingTop, e.Button); + _content.OnPointerReleased(contentE); + } + } + + public override void OnKeyDown(KeyEventArgs e) + { + _content?.OnKeyDown(e); + } + + public override void OnKeyUp(KeyEventArgs e) + { + _content?.OnKeyUp(e); + } + + public override void OnScroll(ScrollEventArgs e) + { + _content?.OnScroll(e); + } + + public override SkiaView? HitTest(float x, float y) + { + if (!IsVisible) + return null; + + // Don't check Bounds.Contains for page - it may not be set + // Just forward to content + + // Check content + if (_content != null) + { + var hit = _content.HitTest(x, y); + if (hit != null) + return hit; + } + + return this; + } +} + +/// +/// Simple content page view with toolbar items support. +/// +public class SkiaContentPage : SkiaPage +{ + private readonly List _toolbarItems = new(); + + /// + /// Gets the toolbar items for this page. + /// + public IList ToolbarItems => _toolbarItems; + + protected override void DrawNavigationBar(SKCanvas canvas, SKRect bounds) + { + // Draw navigation bar background + using var barPaint = new SKPaint + { + Color = TitleBarColor, + Style = SKPaintStyle.Fill + }; + canvas.DrawRect(bounds, barPaint); + + // Draw title + if (!string.IsNullOrEmpty(Title)) + { + using var font = new SKFont(SKTypeface.Default, 20); + using var textPaint = new SKPaint(font) + { + Color = TitleTextColor, + IsAntialias = true + }; + + var textBounds = new SKRect(); + textPaint.MeasureText(Title, ref textBounds); + + var x = bounds.Left + 56; // Leave space for back button + var y = bounds.MidY - textBounds.MidY; + canvas.DrawText(Title, x, y, textPaint); + } + + // Draw toolbar items on the right + DrawToolbarItems(canvas, bounds); + + // Draw shadow + using var shadowPaint = new SKPaint + { + Color = new SKColor(0, 0, 0, 30), + Style = SKPaintStyle.Fill, + MaskFilter = SKMaskFilter.CreateBlur(SKBlurStyle.Normal, 2) + }; + canvas.DrawRect(new SKRect(bounds.Left, bounds.Bottom, bounds.Right, bounds.Bottom + 4), shadowPaint); + } + + private void DrawToolbarItems(SKCanvas canvas, SKRect navBarBounds) + { + var primaryItems = _toolbarItems.Where(t => t.Order == SkiaToolbarItemOrder.Primary).ToList(); + Console.WriteLine($"[SkiaContentPage] DrawToolbarItems: {primaryItems.Count} primary items, navBarBounds={navBarBounds}"); + if (primaryItems.Count == 0) return; + + using var font = new SKFont(SKTypeface.Default, 14); + using var textPaint = new SKPaint(font) + { + Color = TitleTextColor, + IsAntialias = true + }; + + float rightEdge = navBarBounds.Right - 16; + + foreach (var item in primaryItems.AsEnumerable().Reverse()) + { + var textBounds = new SKRect(); + textPaint.MeasureText(item.Text, ref textBounds); + + var itemWidth = textBounds.Width + 24; // Padding + var itemLeft = rightEdge - itemWidth; + + // 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 + } + } + + public override void OnPointerPressed(PointerEventArgs e) + { + Console.WriteLine($"[SkiaContentPage] OnPointerPressed at ({e.X}, {e.Y}), ShowNavigationBar={ShowNavigationBar}, NavigationBarHeight={NavigationBarHeight}"); + Console.WriteLine($"[SkiaContentPage] ToolbarItems count: {_toolbarItems.Count}"); + + // Check toolbar item clicks + if (ShowNavigationBar && e.Y < NavigationBarHeight) + { + Console.WriteLine($"[SkiaContentPage] In navigation bar area, checking toolbar items"); + foreach (var item in _toolbarItems.Where(t => t.Order == SkiaToolbarItemOrder.Primary)) + { + var bounds = item.HitBounds; + var contains = bounds.Contains(e.X, e.Y); + Console.WriteLine($"[SkiaContentPage] Checking item '{item.Text}', HitBounds=({bounds.Left},{bounds.Top},{bounds.Right},{bounds.Bottom}), Click=({e.X},{e.Y}), Contains={contains}, Command={item.Command != null}"); + if (contains) + { + Console.WriteLine($"[SkiaContentPage] Toolbar item clicked: {item.Text}"); + item.Command?.Execute(null); + return; + } + } + Console.WriteLine($"[SkiaContentPage] No toolbar item hit"); + } + + base.OnPointerPressed(e); + } +} + +/// +/// Represents a toolbar item in the navigation bar. +/// +public class SkiaToolbarItem +{ + public string Text { get; set; } = ""; + public SkiaToolbarItemOrder Order { get; set; } = SkiaToolbarItemOrder.Primary; + public System.Windows.Input.ICommand? Command { get; set; } + public SKRect HitBounds { get; set; } +} + +/// +/// Order of toolbar items. +/// +public enum SkiaToolbarItemOrder +{ + Primary, + Secondary } diff --git a/Views/SkiaPicker.cs b/Views/SkiaPicker.cs index 8f1026a..3348f62 100644 --- a/Views/SkiaPicker.cs +++ b/Views/SkiaPicker.cs @@ -1,797 +1,661 @@ -using System; -using System.Collections.Generic; -using Microsoft.Maui.Controls; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using SkiaSharp; namespace Microsoft.Maui.Platform; +/// +/// Skia-rendered picker/dropdown control with full XAML styling support. +/// public class SkiaPicker : SkiaView { - public static readonly BindableProperty SelectedIndexProperty = BindableProperty.Create("SelectedIndex", typeof(int), typeof(SkiaPicker), (object)(-1), (BindingMode)1, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaPicker)(object)b).OnSelectedIndexChanged(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + #region BindableProperties - public static readonly BindableProperty TitleProperty = BindableProperty.Create("Title", typeof(string), typeof(SkiaPicker), (object)"", (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaPicker)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for SelectedIndex. + /// + public static readonly BindableProperty SelectedIndexProperty = + BindableProperty.Create( + nameof(SelectedIndex), + typeof(int), + typeof(SkiaPicker), + -1, + BindingMode.TwoWay, + propertyChanged: (b, o, n) => ((SkiaPicker)b).OnSelectedIndexChanged()); - public static readonly BindableProperty TextColorProperty = BindableProperty.Create("TextColor", typeof(SKColor), typeof(SkiaPicker), (object)SKColors.Black, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaPicker)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for Title. + /// + public static readonly BindableProperty TitleProperty = + BindableProperty.Create( + nameof(Title), + typeof(string), + typeof(SkiaPicker), + "", + propertyChanged: (b, o, n) => ((SkiaPicker)b).Invalidate()); - public static readonly BindableProperty TitleColorProperty = BindableProperty.Create("TitleColor", typeof(SKColor), typeof(SkiaPicker), (object)new SKColor((byte)128, (byte)128, (byte)128), (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaPicker)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for TextColor. + /// + public static readonly BindableProperty TextColorProperty = + BindableProperty.Create( + nameof(TextColor), + typeof(SKColor), + typeof(SkiaPicker), + SKColors.Black, + propertyChanged: (b, o, n) => ((SkiaPicker)b).Invalidate()); - public static readonly BindableProperty BorderColorProperty = BindableProperty.Create("BorderColor", typeof(SKColor), typeof(SkiaPicker), (object)new SKColor((byte)189, (byte)189, (byte)189), (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaPicker)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for TitleColor. + /// + public static readonly BindableProperty TitleColorProperty = + BindableProperty.Create( + nameof(TitleColor), + typeof(SKColor), + typeof(SkiaPicker), + new SKColor(0x80, 0x80, 0x80), + propertyChanged: (b, o, n) => ((SkiaPicker)b).Invalidate()); - public static readonly BindableProperty DropdownBackgroundColorProperty = BindableProperty.Create("DropdownBackgroundColor", typeof(SKColor), typeof(SkiaPicker), (object)SKColors.White, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaPicker)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for BorderColor. + /// + public static readonly BindableProperty BorderColorProperty = + BindableProperty.Create( + nameof(BorderColor), + typeof(SKColor), + typeof(SkiaPicker), + new SKColor(0xBD, 0xBD, 0xBD), + propertyChanged: (b, o, n) => ((SkiaPicker)b).Invalidate()); - public static readonly BindableProperty SelectedItemBackgroundColorProperty = BindableProperty.Create("SelectedItemBackgroundColor", typeof(SKColor), typeof(SkiaPicker), (object)new SKColor((byte)33, (byte)150, (byte)243, (byte)48), (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaPicker)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for DropdownBackgroundColor. + /// + public static readonly BindableProperty DropdownBackgroundColorProperty = + BindableProperty.Create( + nameof(DropdownBackgroundColor), + typeof(SKColor), + typeof(SkiaPicker), + SKColors.White, + propertyChanged: (b, o, n) => ((SkiaPicker)b).Invalidate()); - public static readonly BindableProperty HoverItemBackgroundColorProperty = BindableProperty.Create("HoverItemBackgroundColor", typeof(SKColor), typeof(SkiaPicker), (object)new SKColor((byte)224, (byte)224, (byte)224), (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaPicker)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for SelectedItemBackgroundColor. + /// + public static readonly BindableProperty SelectedItemBackgroundColorProperty = + BindableProperty.Create( + nameof(SelectedItemBackgroundColor), + typeof(SKColor), + typeof(SkiaPicker), + new SKColor(0x21, 0x96, 0xF3, 0x30), + propertyChanged: (b, o, n) => ((SkiaPicker)b).Invalidate()); - public static readonly BindableProperty FontFamilyProperty = BindableProperty.Create("FontFamily", typeof(string), typeof(SkiaPicker), (object)"Sans", (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaPicker)(object)b).InvalidateMeasure(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for HoverItemBackgroundColor. + /// + public static readonly BindableProperty HoverItemBackgroundColorProperty = + BindableProperty.Create( + nameof(HoverItemBackgroundColor), + typeof(SKColor), + typeof(SkiaPicker), + new SKColor(0xE0, 0xE0, 0xE0), + propertyChanged: (b, o, n) => ((SkiaPicker)b).Invalidate()); - public static readonly BindableProperty FontSizeProperty = BindableProperty.Create("FontSize", typeof(float), typeof(SkiaPicker), (object)14f, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaPicker)(object)b).InvalidateMeasure(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for FontFamily. + /// + public static readonly BindableProperty FontFamilyProperty = + BindableProperty.Create( + nameof(FontFamily), + typeof(string), + typeof(SkiaPicker), + "Sans", + propertyChanged: (b, o, n) => ((SkiaPicker)b).InvalidateMeasure()); - public static readonly BindableProperty ItemHeightProperty = BindableProperty.Create("ItemHeight", typeof(float), typeof(SkiaPicker), (object)40f, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaPicker)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for FontSize. + /// + public static readonly BindableProperty FontSizeProperty = + BindableProperty.Create( + nameof(FontSize), + typeof(float), + typeof(SkiaPicker), + 14f, + propertyChanged: (b, o, n) => ((SkiaPicker)b).InvalidateMeasure()); - public static readonly BindableProperty CornerRadiusProperty = BindableProperty.Create("CornerRadius", typeof(float), typeof(SkiaPicker), (object)4f, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaPicker)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for ItemHeight. + /// + public static readonly BindableProperty ItemHeightProperty = + BindableProperty.Create( + nameof(ItemHeight), + typeof(float), + typeof(SkiaPicker), + 40f, + propertyChanged: (b, o, n) => ((SkiaPicker)b).Invalidate()); - private readonly List _items = new List(); + /// + /// Bindable property for CornerRadius. + /// + public static readonly BindableProperty CornerRadiusProperty = + BindableProperty.Create( + nameof(CornerRadius), + typeof(float), + typeof(SkiaPicker), + 4f, + propertyChanged: (b, o, n) => ((SkiaPicker)b).Invalidate()); - private bool _isOpen; + #endregion - private float _dropdownMaxHeight = 200f; + #region Properties - private int _hoveredItemIndex = -1; + /// + /// Gets or sets the selected index. + /// + public int SelectedIndex + { + get => (int)GetValue(SelectedIndexProperty); + set => SetValue(SelectedIndexProperty, value); + } - public int SelectedIndex - { - get - { - return (int)((BindableObject)this).GetValue(SelectedIndexProperty); - } - set - { - ((BindableObject)this).SetValue(SelectedIndexProperty, (object)value); - } - } + /// + /// Gets or sets the title/placeholder. + /// + public string Title + { + get => (string)GetValue(TitleProperty); + set => SetValue(TitleProperty, value); + } - public string Title - { - get - { - return (string)((BindableObject)this).GetValue(TitleProperty); - } - set - { - ((BindableObject)this).SetValue(TitleProperty, (object)value); - } - } + /// + /// Gets or sets the text color. + /// + public SKColor TextColor + { + get => (SKColor)GetValue(TextColorProperty); + set => SetValue(TextColorProperty, value); + } - public SKColor TextColor - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKColor)((BindableObject)this).GetValue(TextColorProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(TextColorProperty, (object)value); - } - } + /// + /// Gets or sets the title color. + /// + public SKColor TitleColor + { + get => (SKColor)GetValue(TitleColorProperty); + set => SetValue(TitleColorProperty, value); + } - public SKColor TitleColor - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKColor)((BindableObject)this).GetValue(TitleColorProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(TitleColorProperty, (object)value); - } - } + /// + /// Gets or sets the border color. + /// + public SKColor BorderColor + { + get => (SKColor)GetValue(BorderColorProperty); + set => SetValue(BorderColorProperty, value); + } - public SKColor BorderColor - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKColor)((BindableObject)this).GetValue(BorderColorProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(BorderColorProperty, (object)value); - } - } + /// + /// Gets or sets the dropdown background color. + /// + public SKColor DropdownBackgroundColor + { + get => (SKColor)GetValue(DropdownBackgroundColorProperty); + set => SetValue(DropdownBackgroundColorProperty, value); + } - public SKColor DropdownBackgroundColor - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKColor)((BindableObject)this).GetValue(DropdownBackgroundColorProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(DropdownBackgroundColorProperty, (object)value); - } - } + /// + /// Gets or sets the selected item background color. + /// + public SKColor SelectedItemBackgroundColor + { + get => (SKColor)GetValue(SelectedItemBackgroundColorProperty); + set => SetValue(SelectedItemBackgroundColorProperty, value); + } - public SKColor SelectedItemBackgroundColor - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKColor)((BindableObject)this).GetValue(SelectedItemBackgroundColorProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(SelectedItemBackgroundColorProperty, (object)value); - } - } + /// + /// Gets or sets the hover item background color. + /// + public SKColor HoverItemBackgroundColor + { + get => (SKColor)GetValue(HoverItemBackgroundColorProperty); + set => SetValue(HoverItemBackgroundColorProperty, value); + } - public SKColor HoverItemBackgroundColor - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKColor)((BindableObject)this).GetValue(HoverItemBackgroundColorProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(HoverItemBackgroundColorProperty, (object)value); - } - } + /// + /// Gets or sets the font family. + /// + public string FontFamily + { + get => (string)GetValue(FontFamilyProperty); + set => SetValue(FontFamilyProperty, value); + } - public string FontFamily - { - get - { - return (string)((BindableObject)this).GetValue(FontFamilyProperty); - } - set - { - ((BindableObject)this).SetValue(FontFamilyProperty, (object)value); - } - } + /// + /// Gets or sets the font size. + /// + public float FontSize + { + get => (float)GetValue(FontSizeProperty); + set => SetValue(FontSizeProperty, value); + } - public float FontSize - { - get - { - return (float)((BindableObject)this).GetValue(FontSizeProperty); - } - set - { - ((BindableObject)this).SetValue(FontSizeProperty, (object)value); - } - } + /// + /// Gets or sets the item height. + /// + public float ItemHeight + { + get => (float)GetValue(ItemHeightProperty); + set => SetValue(ItemHeightProperty, value); + } - public float ItemHeight - { - get - { - return (float)((BindableObject)this).GetValue(ItemHeightProperty); - } - set - { - ((BindableObject)this).SetValue(ItemHeightProperty, (object)value); - } - } + /// + /// Gets or sets the corner radius. + /// + public float CornerRadius + { + get => (float)GetValue(CornerRadiusProperty); + set => SetValue(CornerRadiusProperty, value); + } - public float CornerRadius - { - get - { - return (float)((BindableObject)this).GetValue(CornerRadiusProperty); - } - set - { - ((BindableObject)this).SetValue(CornerRadiusProperty, (object)value); - } - } + /// + /// Gets the items list. + /// + public IList Items => _items; - public IList Items => _items; + /// + /// Gets the selected item. + /// + public string? SelectedItem => SelectedIndex >= 0 && SelectedIndex < _items.Count ? _items[SelectedIndex] : null; - public string? SelectedItem - { - get - { - if (SelectedIndex < 0 || SelectedIndex >= _items.Count) - { - return null; - } - return _items[SelectedIndex]; - } - } + /// + /// Gets or sets whether the dropdown is open. + /// + public bool IsOpen + { + get => _isOpen; + set + { + if (_isOpen != value) + { + _isOpen = value; + if (_isOpen) + { + RegisterPopupOverlay(this, DrawDropdownOverlay); + } + else + { + UnregisterPopupOverlay(this); + } + Invalidate(); + } + } + } - public bool IsOpen - { - get - { - return _isOpen; - } - set - { - if (_isOpen != value) - { - _isOpen = value; - if (_isOpen) - { - SkiaView.RegisterPopupOverlay(this, DrawDropdownOverlay); - } - else - { - SkiaView.UnregisterPopupOverlay(this); - } - Invalidate(); - } - } - } + #endregion - public event EventHandler? SelectedIndexChanged; + private readonly List _items = new(); + private bool _isOpen; + private float _dropdownMaxHeight = 200; + private int _hoveredItemIndex = -1; - public SkiaPicker() - { - base.IsFocusable = true; - } + /// + /// Event raised when selected index changes. + /// + public event EventHandler? SelectedIndexChanged; - private void OnSelectedIndexChanged() - { - this.SelectedIndexChanged?.Invoke(this, EventArgs.Empty); - Invalidate(); - } + public SkiaPicker() + { + IsFocusable = true; + } - public void SetItems(IEnumerable items) - { - _items.Clear(); - _items.AddRange(items); - if (SelectedIndex >= _items.Count) - { - SelectedIndex = ((_items.Count <= 0) ? (-1) : 0); - } - Invalidate(); - } + private void OnSelectedIndexChanged() + { + SelectedIndexChanged?.Invoke(this, EventArgs.Empty); + Invalidate(); + } - private void DrawDropdownOverlay(SKCanvas canvas) - { - //IL_0019: Unknown result type (might be due to invalid IL or missing references) - if (_items.Count != 0 && _isOpen) - { - DrawDropdown(canvas, base.ScreenBounds); - } - } + /// + /// Sets the items in the picker. + /// + public void SetItems(IEnumerable items) + { + _items.Clear(); + _items.AddRange(items); + if (SelectedIndex >= _items.Count) + { + SelectedIndex = _items.Count > 0 ? 0 : -1; + } + Invalidate(); + } - protected override void OnDraw(SKCanvas canvas, SKRect bounds) - { - //IL_0002: Unknown result type (might be due to invalid IL or missing references) - DrawPickerButton(canvas, bounds); - } + private void DrawDropdownOverlay(SKCanvas canvas) + { + if (_items.Count == 0 || !_isOpen) return; + // Use ScreenBounds for overlay drawing to account for scroll offset + DrawDropdown(canvas, ScreenBounds); + } - private void DrawPickerButton(SKCanvas canvas, SKRect bounds) - { - //IL_0000: Unknown result type (might be due to invalid IL or missing references) - //IL_0005: Unknown result type (might be due to invalid IL or missing references) - //IL_0025: Unknown result type (might be due to invalid IL or missing references) - //IL_001d: Unknown result type (might be due to invalid IL or missing references) - //IL_002f: Unknown result type (might be due to invalid IL or missing references) - //IL_0036: Unknown result type (might be due to invalid IL or missing references) - //IL_003e: Expected O, but got Unknown - //IL_003e: Unknown result type (might be due to invalid IL or missing references) - //IL_0045: Unknown result type (might be due to invalid IL or missing references) - //IL_004b: Expected O, but got Unknown - //IL_0053: Unknown result type (might be due to invalid IL or missing references) - //IL_0058: Unknown result type (might be due to invalid IL or missing references) - //IL_0075: Unknown result type (might be due to invalid IL or missing references) - //IL_0062: Unknown result type (might be due to invalid IL or missing references) - //IL_007f: Unknown result type (might be due to invalid IL or missing references) - //IL_0086: Unknown result type (might be due to invalid IL or missing references) - //IL_0099: Unknown result type (might be due to invalid IL or missing references) - //IL_00a1: Expected O, but got Unknown - //IL_00be: Unknown result type (might be due to invalid IL or missing references) - //IL_00c4: Expected O, but got Unknown - //IL_00c5: Unknown result type (might be due to invalid IL or missing references) - //IL_00ca: Unknown result type (might be due to invalid IL or missing references) - //IL_00d3: Expected O, but got Unknown - //IL_013a: Unknown result type (might be due to invalid IL or missing references) - //IL_0146: Unknown result type (might be due to invalid IL or missing references) - //IL_0188: Unknown result type (might be due to invalid IL or missing references) - //IL_0123: Unknown result type (might be due to invalid IL or missing references) - //IL_010d: Unknown result type (might be due to invalid IL or missing references) - //IL_0112: Unknown result type (might be due to invalid IL or missing references) - //IL_011b: Unknown result type (might be due to invalid IL or missing references) - SKPaint val = new SKPaint - { - Color = (SKColor)(base.IsEnabled ? base.BackgroundColor : new SKColor((byte)245, (byte)245, (byte)245)), - Style = (SKPaintStyle)0, - IsAntialias = true - }; - try - { - SKRoundRect val2 = new SKRoundRect(bounds, CornerRadius); - canvas.DrawRoundRect(val2, val); - SKPaint val3 = new SKPaint - { - Color = (SKColor)(base.IsFocused ? new SKColor((byte)33, (byte)150, (byte)243) : BorderColor), - Style = (SKPaintStyle)1, - StrokeWidth = ((!base.IsFocused) ? 1 : 2), - IsAntialias = true - }; - try - { - canvas.DrawRoundRect(val2, val3); - SKFont val4 = new SKFont(SKTypeface.Default, FontSize, 1f, 0f); - try - { - SKPaint val5 = new SKPaint(val4) - { - IsAntialias = true - }; - try - { - string text; - if (SelectedIndex >= 0 && SelectedIndex < _items.Count) - { - text = _items[SelectedIndex]; - SKColor color; - if (!base.IsEnabled) - { - SKColor textColor = TextColor; - color = ((SKColor)(ref textColor)).WithAlpha((byte)128); - } - else - { - color = TextColor; - } - val5.Color = color; - } - else - { - text = Title; - val5.Color = TitleColor; - } - SKRect val6 = default(SKRect); - val5.MeasureText(text, ref val6); - float num = ((SKRect)(ref bounds)).Left + 12f; - float num2 = ((SKRect)(ref bounds)).MidY - ((SKRect)(ref val6)).MidY; - canvas.DrawText(text, num, num2, val5); - DrawDropdownArrow(canvas, bounds); - } - finally - { - ((IDisposable)val5)?.Dispose(); - } - } - finally - { - ((IDisposable)val4)?.Dispose(); - } - } - finally - { - ((IDisposable)val3)?.Dispose(); - } - } - finally - { - ((IDisposable)val)?.Dispose(); - } - } + protected override void OnDraw(SKCanvas canvas, SKRect bounds) + { + DrawPickerButton(canvas, bounds); + } - private void DrawDropdownArrow(SKCanvas canvas, SKRect bounds) - { - //IL_0000: Unknown result type (might be due to invalid IL or missing references) - //IL_0005: Unknown result type (might be due to invalid IL or missing references) - //IL_0025: Unknown result type (might be due to invalid IL or missing references) - //IL_000f: Unknown result type (might be due to invalid IL or missing references) - //IL_0014: Unknown result type (might be due to invalid IL or missing references) - //IL_001d: Unknown result type (might be due to invalid IL or missing references) - //IL_002f: Unknown result type (might be due to invalid IL or missing references) - //IL_0036: Unknown result type (might be due to invalid IL or missing references) - //IL_0041: Unknown result type (might be due to invalid IL or missing references) - //IL_0048: Unknown result type (might be due to invalid IL or missing references) - //IL_0050: Expected O, but got Unknown - //IL_006c: Unknown result type (might be due to invalid IL or missing references) - //IL_0073: Expected O, but got Unknown - SKPaint val = new SKPaint(); - SKColor color; - if (!base.IsEnabled) - { - SKColor textColor = TextColor; - color = ((SKColor)(ref textColor)).WithAlpha((byte)128); - } - else - { - color = TextColor; - } - val.Color = color; - val.Style = (SKPaintStyle)1; - val.StrokeWidth = 2f; - val.IsAntialias = true; - val.StrokeCap = (SKStrokeCap)1; - SKPaint val2 = val; - try - { - float num = 6f; - float num2 = ((SKRect)(ref bounds)).Right - 20f; - float midY = ((SKRect)(ref bounds)).MidY; - SKPath val3 = new SKPath(); - try - { - if (_isOpen) - { - val3.MoveTo(num2 - num, midY + num / 2f); - val3.LineTo(num2, midY - num / 2f); - val3.LineTo(num2 + num, midY + num / 2f); - } - else - { - val3.MoveTo(num2 - num, midY - num / 2f); - val3.LineTo(num2, midY + num / 2f); - val3.LineTo(num2 + num, midY - num / 2f); - } - canvas.DrawPath(val3, val2); - } - finally - { - ((IDisposable)val3)?.Dispose(); - } - } - finally - { - ((IDisposable)val2)?.Dispose(); - } - } + private void DrawPickerButton(SKCanvas canvas, SKRect bounds) + { + // Draw background + using var bgPaint = new SKPaint + { + Color = IsEnabled ? BackgroundColor : new SKColor(0xF5, 0xF5, 0xF5), + Style = SKPaintStyle.Fill, + IsAntialias = true + }; - private void DrawDropdown(SKCanvas canvas, SKRect bounds) - { - //IL_005e: Unknown result type (might be due to invalid IL or missing references) - //IL_0063: Unknown result type (might be due to invalid IL or missing references) - //IL_0069: Unknown result type (might be due to invalid IL or missing references) - //IL_0073: Unknown result type (might be due to invalid IL or missing references) - //IL_0084: Unknown result type (might be due to invalid IL or missing references) - //IL_008c: Expected O, but got Unknown - //IL_00c0: Unknown result type (might be due to invalid IL or missing references) - //IL_00c5: Unknown result type (might be due to invalid IL or missing references) - //IL_00c7: Unknown result type (might be due to invalid IL or missing references) - //IL_00ce: Unknown result type (might be due to invalid IL or missing references) - //IL_00d9: Expected O, but got Unknown - //IL_00d9: Unknown result type (might be due to invalid IL or missing references) - //IL_00de: Unknown result type (might be due to invalid IL or missing references) - //IL_00e0: Unknown result type (might be due to invalid IL or missing references) - //IL_00ea: Unknown result type (might be due to invalid IL or missing references) - //IL_00f1: Unknown result type (might be due to invalid IL or missing references) - //IL_00fa: Expected O, but got Unknown - //IL_00fb: Unknown result type (might be due to invalid IL or missing references) - //IL_0102: Unknown result type (might be due to invalid IL or missing references) - //IL_010e: Expected O, but got Unknown - //IL_010e: Unknown result type (might be due to invalid IL or missing references) - //IL_0113: Unknown result type (might be due to invalid IL or missing references) - //IL_0115: Unknown result type (might be due to invalid IL or missing references) - //IL_011f: Unknown result type (might be due to invalid IL or missing references) - //IL_0126: Unknown result type (might be due to invalid IL or missing references) - //IL_0131: Unknown result type (might be due to invalid IL or missing references) - //IL_013a: Expected O, but got Unknown - //IL_013b: Unknown result type (might be due to invalid IL or missing references) - //IL_0142: Unknown result type (might be due to invalid IL or missing references) - //IL_014e: Expected O, but got Unknown - //IL_0156: Unknown result type (might be due to invalid IL or missing references) - //IL_015d: Unknown result type (might be due to invalid IL or missing references) - //IL_0169: Expected O, but got Unknown - //IL_017e: Unknown result type (might be due to invalid IL or missing references) - //IL_0185: Expected O, but got Unknown - //IL_0187: Unknown result type (might be due to invalid IL or missing references) - //IL_018c: Unknown result type (might be due to invalid IL or missing references) - //IL_018e: Unknown result type (might be due to invalid IL or missing references) - //IL_0198: Unknown result type (might be due to invalid IL or missing references) - //IL_01a1: Expected O, but got Unknown - //IL_01f5: Unknown result type (might be due to invalid IL or missing references) - //IL_01fa: Unknown result type (might be due to invalid IL or missing references) - //IL_01fc: Unknown result type (might be due to invalid IL or missing references) - //IL_0206: Unknown result type (might be due to invalid IL or missing references) - //IL_020f: Expected O, but got Unknown - //IL_0265: Unknown result type (might be due to invalid IL or missing references) - //IL_0231: Unknown result type (might be due to invalid IL or missing references) - //IL_0236: Unknown result type (might be due to invalid IL or missing references) - //IL_0238: Unknown result type (might be due to invalid IL or missing references) - //IL_0242: Unknown result type (might be due to invalid IL or missing references) - //IL_024b: Expected O, but got Unknown - //IL_0210: Unknown result type (might be due to invalid IL or missing references) - //IL_024c: Unknown result type (might be due to invalid IL or missing references) - if (_items.Count == 0) - { - return; - } - float num = Math.Min((float)_items.Count * ItemHeight, _dropdownMaxHeight); - SKRect val = default(SKRect); - ((SKRect)(ref val))._002Ector(((SKRect)(ref bounds)).Left, ((SKRect)(ref bounds)).Bottom + 4f, ((SKRect)(ref bounds)).Right, ((SKRect)(ref bounds)).Bottom + 4f + num); - SKPaint val2 = new SKPaint - { - Color = new SKColor((byte)0, (byte)0, (byte)0, (byte)40), - MaskFilter = SKMaskFilter.CreateBlur((SKBlurStyle)0, 4f), - Style = (SKPaintStyle)0 - }; - try - { - SKRect val3 = new SKRect(((SKRect)(ref val)).Left + 2f, ((SKRect)(ref val)).Top + 2f, ((SKRect)(ref val)).Right + 2f, ((SKRect)(ref val)).Bottom + 2f); - canvas.DrawRoundRect(new SKRoundRect(val3, CornerRadius), val2); - SKPaint val4 = new SKPaint - { - Color = DropdownBackgroundColor, - Style = (SKPaintStyle)0, - IsAntialias = true - }; - try - { - canvas.DrawRoundRect(new SKRoundRect(val, CornerRadius), val4); - SKPaint val5 = new SKPaint - { - Color = BorderColor, - Style = (SKPaintStyle)1, - StrokeWidth = 1f, - IsAntialias = true - }; - try - { - canvas.DrawRoundRect(new SKRoundRect(val, CornerRadius), val5); - canvas.Save(); - canvas.ClipRoundRect(new SKRoundRect(val, CornerRadius), (SKClipOperation)1, false); - SKFont val6 = new SKFont(SKTypeface.Default, FontSize, 1f, 0f); - try - { - SKPaint val7 = new SKPaint(val6) - { - Color = TextColor, - IsAntialias = true - }; - try - { - SKRect val8 = default(SKRect); - for (int i = 0; i < _items.Count; i++) - { - float num2 = ((SKRect)(ref val)).Top + (float)i * ItemHeight; - if (num2 > ((SKRect)(ref val)).Bottom) - { - break; - } - ((SKRect)(ref val8))._002Ector(((SKRect)(ref val)).Left, num2, ((SKRect)(ref val)).Right, num2 + ItemHeight); - if (i == SelectedIndex) - { - SKPaint val9 = new SKPaint - { - Color = SelectedItemBackgroundColor, - Style = (SKPaintStyle)0 - }; - try - { - canvas.DrawRect(val8, val9); - } - finally - { - ((IDisposable)val9)?.Dispose(); - } - } - else if (i == _hoveredItemIndex) - { - SKPaint val10 = new SKPaint - { - Color = HoverItemBackgroundColor, - Style = (SKPaintStyle)0 - }; - try - { - canvas.DrawRect(val8, val10); - } - finally - { - ((IDisposable)val10)?.Dispose(); - } - } - SKRect val11 = default(SKRect); - val7.MeasureText(_items[i], ref val11); - float num3 = ((SKRect)(ref val8)).Left + 12f; - float num4 = ((SKRect)(ref val8)).MidY - ((SKRect)(ref val11)).MidY; - canvas.DrawText(_items[i], num3, num4, val7); - } - canvas.Restore(); - } - finally - { - ((IDisposable)val7)?.Dispose(); - } - } - finally - { - ((IDisposable)val6)?.Dispose(); - } - } - finally - { - ((IDisposable)val5)?.Dispose(); - } - } - finally - { - ((IDisposable)val4)?.Dispose(); - } - } - finally - { - ((IDisposable)val2)?.Dispose(); - } - } + var buttonRect = new SKRoundRect(bounds, CornerRadius); + canvas.DrawRoundRect(buttonRect, bgPaint); - public override void OnPointerPressed(PointerEventArgs e) - { - //IL_0012: Unknown result type (might be due to invalid IL or missing references) - //IL_0017: Unknown result type (might be due to invalid IL or missing references) - if (!base.IsEnabled) - { - return; - } - if (IsOpen) - { - SKRect screenBounds = base.ScreenBounds; - float num = ((SKRect)(ref screenBounds)).Bottom + 4f; - if (e.Y >= num) - { - int num2 = (int)((e.Y - num) / ItemHeight); - if (num2 >= 0 && num2 < _items.Count) - { - SelectedIndex = num2; - } - } - IsOpen = false; - } - else - { - IsOpen = true; - } - Invalidate(); - } + // Draw border + using var borderPaint = new SKPaint + { + Color = IsFocused ? new SKColor(0x21, 0x96, 0xF3) : BorderColor, + Style = SKPaintStyle.Stroke, + StrokeWidth = IsFocused ? 2 : 1, + IsAntialias = true + }; + canvas.DrawRoundRect(buttonRect, borderPaint); - public override void OnPointerMoved(PointerEventArgs e) - { - //IL_000a: Unknown result type (might be due to invalid IL or missing references) - //IL_000f: Unknown result type (might be due to invalid IL or missing references) - if (!_isOpen) - { - return; - } - SKRect screenBounds = base.ScreenBounds; - float num = ((SKRect)(ref screenBounds)).Bottom + 4f; - if (e.Y >= num) - { - int num2 = (int)((e.Y - num) / ItemHeight); - if (num2 != _hoveredItemIndex && num2 >= 0 && num2 < _items.Count) - { - _hoveredItemIndex = num2; - Invalidate(); - } - } - else if (_hoveredItemIndex != -1) - { - _hoveredItemIndex = -1; - Invalidate(); - } - } + // Draw text or title + using var font = new SKFont(SKTypeface.Default, FontSize); + using var textPaint = new SKPaint(font) + { + IsAntialias = true + }; - public override void OnPointerExited(PointerEventArgs e) - { - _hoveredItemIndex = -1; - Invalidate(); - } + string displayText; + if (SelectedIndex >= 0 && SelectedIndex < _items.Count) + { + displayText = _items[SelectedIndex]; + textPaint.Color = IsEnabled ? TextColor : TextColor.WithAlpha(128); + } + else + { + displayText = Title; + textPaint.Color = TitleColor; + } - public override void OnKeyDown(KeyEventArgs e) - { - if (!base.IsEnabled) - { - return; - } - switch (e.Key) - { - case Key.Enter: - case Key.Space: - IsOpen = !IsOpen; - e.Handled = true; - Invalidate(); - break; - case Key.Escape: - if (IsOpen) - { - IsOpen = false; - e.Handled = true; - Invalidate(); - } - break; - case Key.Up: - if (SelectedIndex > 0) - { - SelectedIndex--; - e.Handled = true; - } - break; - case Key.Down: - if (SelectedIndex < _items.Count - 1) - { - SelectedIndex++; - e.Handled = true; - } - break; - } - } + var textBounds = new SKRect(); + textPaint.MeasureText(displayText, ref textBounds); - public override void OnFocusLost() - { - base.OnFocusLost(); - if (IsOpen) - { - IsOpen = false; - } - } + var textX = bounds.Left + 12; + var textY = bounds.MidY - textBounds.MidY; + canvas.DrawText(displayText, textX, textY, textPaint); - protected override SKSize MeasureOverride(SKSize availableSize) - { - //IL_002b: Unknown result type (might be due to invalid IL or missing references) - return new SKSize((((SKSize)(ref availableSize)).Width < float.MaxValue) ? Math.Min(((SKSize)(ref availableSize)).Width, 200f) : 200f, 40f); - } + // Draw dropdown arrow + DrawDropdownArrow(canvas, bounds); + } - protected override bool HitTestPopupArea(float x, float y) - { - //IL_0001: Unknown result type (might be due to invalid IL or missing references) - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - SKRect screenBounds = base.ScreenBounds; - if (((SKRect)(ref screenBounds)).Contains(x, y)) - { - return true; - } - if (_isOpen && _items.Count > 0) - { - float num = Math.Min((float)_items.Count * ItemHeight, _dropdownMaxHeight); - SKRect val = default(SKRect); - ((SKRect)(ref val))._002Ector(((SKRect)(ref screenBounds)).Left, ((SKRect)(ref screenBounds)).Bottom + 4f, ((SKRect)(ref screenBounds)).Right, ((SKRect)(ref screenBounds)).Bottom + 4f + num); - return ((SKRect)(ref val)).Contains(x, y); - } - return false; - } + private void DrawDropdownArrow(SKCanvas canvas, SKRect bounds) + { + using var paint = new SKPaint + { + Color = IsEnabled ? TextColor : TextColor.WithAlpha(128), + Style = SKPaintStyle.Stroke, + StrokeWidth = 2, + IsAntialias = true, + StrokeCap = SKStrokeCap.Round + }; + + var arrowSize = 6f; + var centerX = bounds.Right - 20; + var centerY = bounds.MidY; + + using var path = new SKPath(); + if (_isOpen) + { + path.MoveTo(centerX - arrowSize, centerY + arrowSize / 2); + path.LineTo(centerX, centerY - arrowSize / 2); + path.LineTo(centerX + arrowSize, centerY + arrowSize / 2); + } + else + { + path.MoveTo(centerX - arrowSize, centerY - arrowSize / 2); + path.LineTo(centerX, centerY + arrowSize / 2); + path.LineTo(centerX + arrowSize, centerY - arrowSize / 2); + } + + canvas.DrawPath(path, paint); + } + + private void DrawDropdown(SKCanvas canvas, SKRect bounds) + { + if (_items.Count == 0) return; + + var dropdownHeight = Math.Min(_items.Count * ItemHeight, _dropdownMaxHeight); + var dropdownRect = new SKRect( + bounds.Left, + bounds.Bottom + 4, + bounds.Right, + bounds.Bottom + 4 + dropdownHeight); + + // Draw shadow + using var shadowPaint = new SKPaint + { + Color = new SKColor(0, 0, 0, 40), + MaskFilter = SKMaskFilter.CreateBlur(SKBlurStyle.Normal, 4), + Style = SKPaintStyle.Fill + }; + var shadowRect = new SKRect(dropdownRect.Left + 2, dropdownRect.Top + 2, dropdownRect.Right + 2, dropdownRect.Bottom + 2); + canvas.DrawRoundRect(new SKRoundRect(shadowRect, CornerRadius), shadowPaint); + + // Draw dropdown background + using var bgPaint = new SKPaint + { + Color = DropdownBackgroundColor, + Style = SKPaintStyle.Fill, + IsAntialias = true + }; + canvas.DrawRoundRect(new SKRoundRect(dropdownRect, CornerRadius), bgPaint); + + // Draw border + using var borderPaint = new SKPaint + { + Color = BorderColor, + Style = SKPaintStyle.Stroke, + StrokeWidth = 1, + IsAntialias = true + }; + canvas.DrawRoundRect(new SKRoundRect(dropdownRect, CornerRadius), borderPaint); + + // Clip to dropdown bounds + canvas.Save(); + canvas.ClipRoundRect(new SKRoundRect(dropdownRect, CornerRadius)); + + // Draw items + using var font = new SKFont(SKTypeface.Default, FontSize); + using var textPaint = new SKPaint(font) + { + Color = TextColor, + IsAntialias = true + }; + + for (int i = 0; i < _items.Count; i++) + { + var itemTop = dropdownRect.Top + i * ItemHeight; + if (itemTop > dropdownRect.Bottom) break; + + var itemRect = new SKRect(dropdownRect.Left, itemTop, dropdownRect.Right, itemTop + ItemHeight); + + // Draw item background + if (i == SelectedIndex) + { + using var selectedPaint = new SKPaint + { + Color = SelectedItemBackgroundColor, + Style = SKPaintStyle.Fill + }; + canvas.DrawRect(itemRect, selectedPaint); + } + else if (i == _hoveredItemIndex) + { + using var hoverPaint = new SKPaint + { + Color = HoverItemBackgroundColor, + Style = SKPaintStyle.Fill + }; + canvas.DrawRect(itemRect, hoverPaint); + } + + // Draw item text + var textBounds = new SKRect(); + textPaint.MeasureText(_items[i], ref textBounds); + + var textX = itemRect.Left + 12; + var textY = itemRect.MidY - textBounds.MidY; + canvas.DrawText(_items[i], textX, textY, textPaint); + } + + canvas.Restore(); + } + + public override void OnPointerPressed(PointerEventArgs e) + { + if (!IsEnabled) return; + + if (IsOpen) + { + // Use ScreenBounds for popup coordinate calculations (accounts for scroll offset) + var screenBounds = ScreenBounds; + var dropdownTop = screenBounds.Bottom + 4; + if (e.Y >= dropdownTop) + { + var itemIndex = (int)((e.Y - dropdownTop) / ItemHeight); + if (itemIndex >= 0 && itemIndex < _items.Count) + { + SelectedIndex = itemIndex; + } + } + IsOpen = false; + } + else + { + IsOpen = true; + } + + Invalidate(); + } + + public override void OnPointerMoved(PointerEventArgs e) + { + if (!_isOpen) return; + + // Use ScreenBounds for popup coordinate calculations (accounts for scroll offset) + var screenBounds = ScreenBounds; + var dropdownTop = screenBounds.Bottom + 4; + if (e.Y >= dropdownTop) + { + var newHovered = (int)((e.Y - dropdownTop) / ItemHeight); + if (newHovered != _hoveredItemIndex && newHovered >= 0 && newHovered < _items.Count) + { + _hoveredItemIndex = newHovered; + Invalidate(); + } + } + else + { + if (_hoveredItemIndex != -1) + { + _hoveredItemIndex = -1; + Invalidate(); + } + } + } + + public override void OnPointerExited(PointerEventArgs e) + { + _hoveredItemIndex = -1; + Invalidate(); + } + + public override void OnKeyDown(KeyEventArgs e) + { + if (!IsEnabled) return; + + switch (e.Key) + { + case Key.Enter: + case Key.Space: + IsOpen = !IsOpen; + e.Handled = true; + Invalidate(); + break; + + case Key.Escape: + if (IsOpen) + { + IsOpen = false; + e.Handled = true; + Invalidate(); + } + break; + + case Key.Up: + if (SelectedIndex > 0) + { + SelectedIndex--; + e.Handled = true; + } + break; + + case Key.Down: + if (SelectedIndex < _items.Count - 1) + { + SelectedIndex++; + e.Handled = true; + } + break; + } + } + + public override void OnFocusLost() + { + base.OnFocusLost(); + if (IsOpen) + { + IsOpen = false; + } + } + + protected override SKSize MeasureOverride(SKSize availableSize) + { + return new SKSize( + availableSize.Width < float.MaxValue ? Math.Min(availableSize.Width, 200) : 200, + 40); + } + + /// + /// Override to include dropdown area in hit testing. + /// + protected override bool HitTestPopupArea(float x, float y) + { + // Use ScreenBounds for hit testing (accounts for scroll offset) + var screenBounds = ScreenBounds; + + // Always include the picker button itself + if (screenBounds.Contains(x, y)) + return true; + + // When open, also include the dropdown area + if (_isOpen && _items.Count > 0) + { + var dropdownHeight = Math.Min(_items.Count * ItemHeight, _dropdownMaxHeight); + var dropdownRect = new SKRect( + screenBounds.Left, + screenBounds.Bottom + 4, + screenBounds.Right, + screenBounds.Bottom + 4 + dropdownHeight); + + return dropdownRect.Contains(x, y); + } + + return false; + } } diff --git a/Views/SkiaProgressBar.cs b/Views/SkiaProgressBar.cs index 9b43424..ea7f7b6 100644 --- a/Views/SkiaProgressBar.cs +++ b/Views/SkiaProgressBar.cs @@ -1,192 +1,205 @@ -using System; -using Microsoft.Maui.Controls; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using SkiaSharp; namespace Microsoft.Maui.Platform; +/// +/// Skia-rendered progress bar control with full XAML styling support. +/// public class SkiaProgressBar : SkiaView { - public static readonly BindableProperty ProgressProperty = BindableProperty.Create("Progress", typeof(double), typeof(SkiaProgressBar), (object)0.0, (BindingMode)1, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaProgressBar)(object)b).OnProgressChanged(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)((BindableObject b, object v) => Math.Clamp((double)v, 0.0, 1.0)), (CreateDefaultValueDelegate)null); + #region BindableProperties - public static readonly BindableProperty TrackColorProperty = BindableProperty.Create("TrackColor", typeof(SKColor), typeof(SkiaProgressBar), (object)new SKColor((byte)224, (byte)224, (byte)224), (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaProgressBar)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for Progress. + /// + public static readonly BindableProperty ProgressProperty = + BindableProperty.Create( + nameof(Progress), + typeof(double), + typeof(SkiaProgressBar), + 0.0, + BindingMode.TwoWay, + coerceValue: (b, v) => Math.Clamp((double)v, 0, 1), + propertyChanged: (b, o, n) => ((SkiaProgressBar)b).OnProgressChanged()); - public static readonly BindableProperty ProgressColorProperty = BindableProperty.Create("ProgressColor", typeof(SKColor), typeof(SkiaProgressBar), (object)new SKColor((byte)33, (byte)150, (byte)243), (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaProgressBar)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for TrackColor. + /// + public static readonly BindableProperty TrackColorProperty = + BindableProperty.Create( + nameof(TrackColor), + typeof(SKColor), + typeof(SkiaProgressBar), + new SKColor(0xE0, 0xE0, 0xE0), + propertyChanged: (b, o, n) => ((SkiaProgressBar)b).Invalidate()); - public static readonly BindableProperty DisabledColorProperty = BindableProperty.Create("DisabledColor", typeof(SKColor), typeof(SkiaProgressBar), (object)new SKColor((byte)189, (byte)189, (byte)189), (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaProgressBar)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for ProgressColor. + /// + public static readonly BindableProperty ProgressColorProperty = + BindableProperty.Create( + nameof(ProgressColor), + typeof(SKColor), + typeof(SkiaProgressBar), + new SKColor(0x21, 0x96, 0xF3), + propertyChanged: (b, o, n) => ((SkiaProgressBar)b).Invalidate()); - public static readonly BindableProperty BarHeightProperty = BindableProperty.Create("BarHeight", typeof(float), typeof(SkiaProgressBar), (object)4f, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaProgressBar)(object)b).InvalidateMeasure(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for DisabledColor. + /// + public static readonly BindableProperty DisabledColorProperty = + BindableProperty.Create( + nameof(DisabledColor), + typeof(SKColor), + typeof(SkiaProgressBar), + new SKColor(0xBD, 0xBD, 0xBD), + propertyChanged: (b, o, n) => ((SkiaProgressBar)b).Invalidate()); - public static readonly BindableProperty CornerRadiusProperty = BindableProperty.Create("CornerRadius", typeof(float), typeof(SkiaProgressBar), (object)2f, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaProgressBar)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for BarHeight. + /// + public static readonly BindableProperty BarHeightProperty = + BindableProperty.Create( + nameof(BarHeight), + typeof(float), + typeof(SkiaProgressBar), + 4f, + propertyChanged: (b, o, n) => ((SkiaProgressBar)b).InvalidateMeasure()); - public double Progress - { - get - { - return (double)((BindableObject)this).GetValue(ProgressProperty); - } - set - { - ((BindableObject)this).SetValue(ProgressProperty, (object)value); - } - } + /// + /// Bindable property for CornerRadius. + /// + public static readonly BindableProperty CornerRadiusProperty = + BindableProperty.Create( + nameof(CornerRadius), + typeof(float), + typeof(SkiaProgressBar), + 2f, + propertyChanged: (b, o, n) => ((SkiaProgressBar)b).Invalidate()); - public SKColor TrackColor - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKColor)((BindableObject)this).GetValue(TrackColorProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(TrackColorProperty, (object)value); - } - } + #endregion - public SKColor ProgressColor - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKColor)((BindableObject)this).GetValue(ProgressColorProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(ProgressColorProperty, (object)value); - } - } + #region Properties - public SKColor DisabledColor - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKColor)((BindableObject)this).GetValue(DisabledColorProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(DisabledColorProperty, (object)value); - } - } + /// + /// Gets or sets the progress value (0.0 to 1.0). + /// + public double Progress + { + get => (double)GetValue(ProgressProperty); + set => SetValue(ProgressProperty, value); + } - public float BarHeight - { - get - { - return (float)((BindableObject)this).GetValue(BarHeightProperty); - } - set - { - ((BindableObject)this).SetValue(BarHeightProperty, (object)value); - } - } + /// + /// Gets or sets the track color. + /// + public SKColor TrackColor + { + get => (SKColor)GetValue(TrackColorProperty); + set => SetValue(TrackColorProperty, value); + } - public float CornerRadius - { - get - { - return (float)((BindableObject)this).GetValue(CornerRadiusProperty); - } - set - { - ((BindableObject)this).SetValue(CornerRadiusProperty, (object)value); - } - } + /// + /// Gets or sets the progress color. + /// + public SKColor ProgressColor + { + get => (SKColor)GetValue(ProgressColorProperty); + set => SetValue(ProgressColorProperty, value); + } - public event EventHandler? ProgressChanged; + /// + /// Gets or sets the disabled color. + /// + public SKColor DisabledColor + { + get => (SKColor)GetValue(DisabledColorProperty); + set => SetValue(DisabledColorProperty, value); + } - private void OnProgressChanged() - { - this.ProgressChanged?.Invoke(this, new ProgressChangedEventArgs(Progress)); - Invalidate(); - } + /// + /// Gets or sets the bar height. + /// + public float BarHeight + { + get => (float)GetValue(BarHeightProperty); + set => SetValue(BarHeightProperty, value); + } - protected override void OnDraw(SKCanvas canvas, SKRect bounds) - { - //IL_0024: Unknown result type (might be due to invalid IL or missing references) - //IL_0029: Unknown result type (might be due to invalid IL or missing references) - //IL_003b: Unknown result type (might be due to invalid IL or missing references) - //IL_0033: Unknown result type (might be due to invalid IL or missing references) - //IL_0045: Unknown result type (might be due to invalid IL or missing references) - //IL_004c: Unknown result type (might be due to invalid IL or missing references) - //IL_0054: Expected O, but got Unknown - //IL_0064: Unknown result type (might be due to invalid IL or missing references) - //IL_006f: Unknown result type (might be due to invalid IL or missing references) - //IL_0075: Expected O, but got Unknown - //IL_009f: Unknown result type (might be due to invalid IL or missing references) - //IL_00a4: Unknown result type (might be due to invalid IL or missing references) - //IL_00b6: Unknown result type (might be due to invalid IL or missing references) - //IL_00ae: Unknown result type (might be due to invalid IL or missing references) - //IL_00c0: Unknown result type (might be due to invalid IL or missing references) - //IL_00c7: Unknown result type (might be due to invalid IL or missing references) - //IL_00d0: Expected O, but got Unknown - //IL_00e3: Unknown result type (might be due to invalid IL or missing references) - //IL_00ee: Unknown result type (might be due to invalid IL or missing references) - //IL_00f5: Expected O, but got Unknown - float midY = ((SKRect)(ref bounds)).MidY; - float num = midY - BarHeight / 2f; - float num2 = midY + BarHeight / 2f; - SKPaint val = new SKPaint - { - Color = (base.IsEnabled ? TrackColor : DisabledColor), - IsAntialias = true, - Style = (SKPaintStyle)0 - }; - try - { - SKRoundRect val2 = new SKRoundRect(new SKRect(((SKRect)(ref bounds)).Left, num, ((SKRect)(ref bounds)).Right, num2), CornerRadius); - canvas.DrawRoundRect(val2, val); - if (Progress > 0.0) - { - float num3 = ((SKRect)(ref bounds)).Width * (float)Progress; - SKPaint val3 = new SKPaint - { - Color = (base.IsEnabled ? ProgressColor : DisabledColor), - IsAntialias = true, - Style = (SKPaintStyle)0 - }; - try - { - SKRoundRect val4 = new SKRoundRect(new SKRect(((SKRect)(ref bounds)).Left, num, ((SKRect)(ref bounds)).Left + num3, num2), CornerRadius); - canvas.DrawRoundRect(val4, val3); - return; - } - finally - { - ((IDisposable)val3)?.Dispose(); - } - } - } - finally - { - ((IDisposable)val)?.Dispose(); - } - } + /// + /// Gets or sets the corner radius. + /// + public float CornerRadius + { + get => (float)GetValue(CornerRadiusProperty); + set => SetValue(CornerRadiusProperty, value); + } - protected override SKSize MeasureOverride(SKSize availableSize) - { - //IL_0011: Unknown result type (might be due to invalid IL or missing references) - return new SKSize(200f, BarHeight + 8f); - } + #endregion + + /// + /// Event raised when progress changes. + /// + public event EventHandler? ProgressChanged; + + private void OnProgressChanged() + { + ProgressChanged?.Invoke(this, new ProgressChangedEventArgs(Progress)); + Invalidate(); + } + + protected override void OnDraw(SKCanvas canvas, SKRect bounds) + { + var trackY = bounds.MidY; + var trackTop = trackY - BarHeight / 2; + var trackBottom = trackY + BarHeight / 2; + + // Draw track + using var trackPaint = new SKPaint + { + Color = IsEnabled ? TrackColor : DisabledColor, + IsAntialias = true, + Style = SKPaintStyle.Fill + }; + + var trackRect = new SKRoundRect( + new SKRect(bounds.Left, trackTop, bounds.Right, trackBottom), + CornerRadius); + canvas.DrawRoundRect(trackRect, trackPaint); + + // Draw progress + if (Progress > 0) + { + var progressWidth = bounds.Width * (float)Progress; + + using var progressPaint = new SKPaint + { + Color = IsEnabled ? ProgressColor : DisabledColor, + IsAntialias = true, + Style = SKPaintStyle.Fill + }; + + var progressRect = new SKRoundRect( + new SKRect(bounds.Left, trackTop, bounds.Left + progressWidth, trackBottom), + CornerRadius); + canvas.DrawRoundRect(progressRect, progressPaint); + } + } + + protected override SKSize MeasureOverride(SKSize availableSize) + { + return new SKSize(200, BarHeight + 8); + } +} + +/// +/// Event args for progress changed events. +/// +public class ProgressChangedEventArgs : EventArgs +{ + public double Progress { get; } + public ProgressChangedEventArgs(double progress) => Progress = progress; } diff --git a/Views/SkiaRadioButton.cs b/Views/SkiaRadioButton.cs index 3ba0458..60a133d 100644 --- a/Views/SkiaRadioButton.cs +++ b/Views/SkiaRadioButton.cs @@ -1,442 +1,284 @@ -using System; -using System.Collections.Generic; -using Microsoft.Maui.Controls; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using SkiaSharp; namespace Microsoft.Maui.Platform; +/// +/// Skia-rendered radio button control with full XAML styling support. +/// public class SkiaRadioButton : SkiaView { - public static readonly BindableProperty IsCheckedProperty = BindableProperty.Create("IsChecked", typeof(bool), typeof(SkiaRadioButton), (object)false, (BindingMode)1, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaRadioButton)(object)b).OnIsCheckedChanged(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + #region BindableProperties - public static readonly BindableProperty ContentProperty = BindableProperty.Create("Content", typeof(string), typeof(SkiaRadioButton), (object)"", (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaRadioButton)(object)b).InvalidateMeasure(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + public static readonly BindableProperty IsCheckedProperty = + BindableProperty.Create(nameof(IsChecked), typeof(bool), typeof(SkiaRadioButton), false, BindingMode.TwoWay, + propertyChanged: (b, o, n) => ((SkiaRadioButton)b).OnIsCheckedChanged()); - public static readonly BindableProperty ValueProperty = BindableProperty.Create("Value", typeof(object), typeof(SkiaRadioButton), (object)null, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)null, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + public static readonly BindableProperty ContentProperty = + BindableProperty.Create(nameof(Content), typeof(string), typeof(SkiaRadioButton), "", + propertyChanged: (b, o, n) => ((SkiaRadioButton)b).InvalidateMeasure()); - public static readonly BindableProperty GroupNameProperty = BindableProperty.Create("GroupName", typeof(string), typeof(SkiaRadioButton), (object)null, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaRadioButton)(object)b).OnGroupNameChanged((string)o, (string)n); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + public static readonly BindableProperty ValueProperty = + BindableProperty.Create(nameof(Value), typeof(object), typeof(SkiaRadioButton), null); - public static readonly BindableProperty RadioColorProperty = BindableProperty.Create("RadioColor", typeof(SKColor), typeof(SkiaRadioButton), (object)new SKColor((byte)33, (byte)150, (byte)243), (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaRadioButton)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + public static readonly BindableProperty GroupNameProperty = + BindableProperty.Create(nameof(GroupName), typeof(string), typeof(SkiaRadioButton), null, + propertyChanged: (b, o, n) => ((SkiaRadioButton)b).OnGroupNameChanged((string?)o, (string?)n)); - public static readonly BindableProperty UncheckedColorProperty = BindableProperty.Create("UncheckedColor", typeof(SKColor), typeof(SkiaRadioButton), (object)new SKColor((byte)117, (byte)117, (byte)117), (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaRadioButton)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + public static readonly BindableProperty RadioColorProperty = + BindableProperty.Create(nameof(RadioColor), typeof(SKColor), typeof(SkiaRadioButton), new SKColor(0x21, 0x96, 0xF3), + propertyChanged: (b, o, n) => ((SkiaRadioButton)b).Invalidate()); - public static readonly BindableProperty TextColorProperty = BindableProperty.Create("TextColor", typeof(SKColor), typeof(SkiaRadioButton), (object)SKColors.Black, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaRadioButton)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + public static readonly BindableProperty UncheckedColorProperty = + BindableProperty.Create(nameof(UncheckedColor), typeof(SKColor), typeof(SkiaRadioButton), new SKColor(0x75, 0x75, 0x75), + propertyChanged: (b, o, n) => ((SkiaRadioButton)b).Invalidate()); - public static readonly BindableProperty DisabledColorProperty = BindableProperty.Create("DisabledColor", typeof(SKColor), typeof(SkiaRadioButton), (object)new SKColor((byte)189, (byte)189, (byte)189), (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaRadioButton)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + public static readonly BindableProperty TextColorProperty = + BindableProperty.Create(nameof(TextColor), typeof(SKColor), typeof(SkiaRadioButton), SKColors.Black, + propertyChanged: (b, o, n) => ((SkiaRadioButton)b).Invalidate()); - public static readonly BindableProperty FontSizeProperty = BindableProperty.Create("FontSize", typeof(float), typeof(SkiaRadioButton), (object)14f, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaRadioButton)(object)b).InvalidateMeasure(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + public static readonly BindableProperty DisabledColorProperty = + BindableProperty.Create(nameof(DisabledColor), typeof(SKColor), typeof(SkiaRadioButton), new SKColor(0xBD, 0xBD, 0xBD), + propertyChanged: (b, o, n) => ((SkiaRadioButton)b).Invalidate()); - public static readonly BindableProperty RadioSizeProperty = BindableProperty.Create("RadioSize", typeof(float), typeof(SkiaRadioButton), (object)20f, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaRadioButton)(object)b).InvalidateMeasure(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + public static readonly BindableProperty FontSizeProperty = + BindableProperty.Create(nameof(FontSize), typeof(float), typeof(SkiaRadioButton), 14f, + propertyChanged: (b, o, n) => ((SkiaRadioButton)b).InvalidateMeasure()); - public static readonly BindableProperty SpacingProperty = BindableProperty.Create("Spacing", typeof(float), typeof(SkiaRadioButton), (object)8f, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaRadioButton)(object)b).InvalidateMeasure(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + public static readonly BindableProperty RadioSizeProperty = + BindableProperty.Create(nameof(RadioSize), typeof(float), typeof(SkiaRadioButton), 20f, + propertyChanged: (b, o, n) => ((SkiaRadioButton)b).InvalidateMeasure()); - private static readonly Dictionary>> _groups = new Dictionary>>(); + public static readonly BindableProperty SpacingProperty = + BindableProperty.Create(nameof(Spacing), typeof(float), typeof(SkiaRadioButton), 8f, + propertyChanged: (b, o, n) => ((SkiaRadioButton)b).InvalidateMeasure()); - public bool IsChecked - { - get - { - return (bool)((BindableObject)this).GetValue(IsCheckedProperty); - } - set - { - ((BindableObject)this).SetValue(IsCheckedProperty, (object)value); - } - } + #endregion - public string Content - { - get - { - return (string)((BindableObject)this).GetValue(ContentProperty); - } - set - { - ((BindableObject)this).SetValue(ContentProperty, (object)value); - } - } + #region Properties - public object? Value - { - get - { - return ((BindableObject)this).GetValue(ValueProperty); - } - set - { - ((BindableObject)this).SetValue(ValueProperty, value); - } - } + public bool IsChecked + { + get => (bool)GetValue(IsCheckedProperty); + set => SetValue(IsCheckedProperty, value); + } - public string? GroupName - { - get - { - return (string)((BindableObject)this).GetValue(GroupNameProperty); - } - set - { - ((BindableObject)this).SetValue(GroupNameProperty, (object)value); - } - } + public string Content + { + get => (string)GetValue(ContentProperty); + set => SetValue(ContentProperty, value); + } - public SKColor RadioColor - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKColor)((BindableObject)this).GetValue(RadioColorProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(RadioColorProperty, (object)value); - } - } + public object? Value + { + get => GetValue(ValueProperty); + set => SetValue(ValueProperty, value); + } - public SKColor UncheckedColor - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKColor)((BindableObject)this).GetValue(UncheckedColorProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(UncheckedColorProperty, (object)value); - } - } + public string? GroupName + { + get => (string?)GetValue(GroupNameProperty); + set => SetValue(GroupNameProperty, value); + } - public SKColor TextColor - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKColor)((BindableObject)this).GetValue(TextColorProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(TextColorProperty, (object)value); - } - } + public SKColor RadioColor + { + get => (SKColor)GetValue(RadioColorProperty); + set => SetValue(RadioColorProperty, value); + } - public SKColor DisabledColor - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKColor)((BindableObject)this).GetValue(DisabledColorProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(DisabledColorProperty, (object)value); - } - } + public SKColor UncheckedColor + { + get => (SKColor)GetValue(UncheckedColorProperty); + set => SetValue(UncheckedColorProperty, value); + } - public float FontSize - { - get - { - return (float)((BindableObject)this).GetValue(FontSizeProperty); - } - set - { - ((BindableObject)this).SetValue(FontSizeProperty, (object)value); - } - } + public SKColor TextColor + { + get => (SKColor)GetValue(TextColorProperty); + set => SetValue(TextColorProperty, value); + } - public float RadioSize - { - get - { - return (float)((BindableObject)this).GetValue(RadioSizeProperty); - } - set - { - ((BindableObject)this).SetValue(RadioSizeProperty, (object)value); - } - } + public SKColor DisabledColor + { + get => (SKColor)GetValue(DisabledColorProperty); + set => SetValue(DisabledColorProperty, value); + } - public float Spacing - { - get - { - return (float)((BindableObject)this).GetValue(SpacingProperty); - } - set - { - ((BindableObject)this).SetValue(SpacingProperty, (object)value); - } - } + public float FontSize + { + get => (float)GetValue(FontSizeProperty); + set => SetValue(FontSizeProperty, value); + } - public event EventHandler? CheckedChanged; + public float RadioSize + { + get => (float)GetValue(RadioSizeProperty); + set => SetValue(RadioSizeProperty, value); + } - public SkiaRadioButton() - { - base.IsFocusable = true; - } + public float Spacing + { + get => (float)GetValue(SpacingProperty); + set => SetValue(SpacingProperty, value); + } - private void OnIsCheckedChanged() - { - if (IsChecked && !string.IsNullOrEmpty(GroupName)) - { - UncheckOthersInGroup(); - } - this.CheckedChanged?.Invoke(this, EventArgs.Empty); - SkiaVisualStateManager.GoToState(this, IsChecked ? "Checked" : "Unchecked"); - Invalidate(); - } + #endregion - private void OnGroupNameChanged(string? oldValue, string? newValue) - { - RemoveFromGroup(oldValue); - AddToGroup(newValue); - } + private static readonly Dictionary>> _groups = new(); - private void AddToGroup(string? groupName) - { - if (!string.IsNullOrEmpty(groupName)) - { - if (!_groups.TryGetValue(groupName, out List> value)) - { - value = new List>(); - _groups[groupName] = value; - } - value.RemoveAll((WeakReference wr) => !wr.TryGetTarget(out SkiaRadioButton _)); - value.Add(new WeakReference(this)); - } - } + public event EventHandler? CheckedChanged; - private void RemoveFromGroup(string? groupName) - { - if (!string.IsNullOrEmpty(groupName) && _groups.TryGetValue(groupName, out List> value)) - { - value.RemoveAll((WeakReference wr) => !wr.TryGetTarget(out SkiaRadioButton target) || target == this); - if (value.Count == 0) - { - _groups.Remove(groupName); - } - } - } + public SkiaRadioButton() + { + IsFocusable = true; + } - private void UncheckOthersInGroup() - { - if (string.IsNullOrEmpty(GroupName) || !_groups.TryGetValue(GroupName, out List> value)) - { - return; - } - foreach (WeakReference item in value) - { - if (item.TryGetTarget(out var target) && target != this && target.IsChecked) - { - ((BindableObject)target).SetValue(IsCheckedProperty, (object)false); - } - } - } + private void OnIsCheckedChanged() + { + if (IsChecked && !string.IsNullOrEmpty(GroupName)) + { + UncheckOthersInGroup(); + } + CheckedChanged?.Invoke(this, EventArgs.Empty); + SkiaVisualStateManager.GoToState(this, IsChecked ? SkiaVisualStateManager.CommonStates.Checked : SkiaVisualStateManager.CommonStates.Unchecked); + Invalidate(); + } - protected override void OnDraw(SKCanvas canvas, SKRect bounds) - { - //IL_001f: Unknown result type (might be due to invalid IL or missing references) - //IL_0024: Unknown result type (might be due to invalid IL or missing references) - //IL_002e: Unknown result type (might be due to invalid IL or missing references) - //IL_0046: Unknown result type (might be due to invalid IL or missing references) - //IL_003e: Unknown result type (might be due to invalid IL or missing references) - //IL_0050: Unknown result type (might be due to invalid IL or missing references) - //IL_0057: Unknown result type (might be due to invalid IL or missing references) - //IL_0062: Unknown result type (might be due to invalid IL or missing references) - //IL_006a: Expected O, but got Unknown - //IL_0082: Unknown result type (might be due to invalid IL or missing references) - //IL_0087: Unknown result type (might be due to invalid IL or missing references) - //IL_00da: Unknown result type (might be due to invalid IL or missing references) - //IL_00df: Unknown result type (might be due to invalid IL or missing references) - //IL_00e1: Unknown result type (might be due to invalid IL or missing references) - //IL_00e6: Unknown result type (might be due to invalid IL or missing references) - //IL_00ec: Unknown result type (might be due to invalid IL or missing references) - //IL_00f6: Unknown result type (might be due to invalid IL or missing references) - //IL_00fd: Unknown result type (might be due to invalid IL or missing references) - //IL_0106: Expected O, but got Unknown - //IL_0099: Unknown result type (might be due to invalid IL or missing references) - //IL_0091: Unknown result type (might be due to invalid IL or missing references) - //IL_014a: Unknown result type (might be due to invalid IL or missing references) - //IL_0151: Expected O, but got Unknown - //IL_00a3: Unknown result type (might be due to invalid IL or missing references) - //IL_00aa: Unknown result type (might be due to invalid IL or missing references) - //IL_00b3: Expected O, but got Unknown - //IL_0153: Unknown result type (might be due to invalid IL or missing references) - //IL_0158: Unknown result type (might be due to invalid IL or missing references) - //IL_016a: Unknown result type (might be due to invalid IL or missing references) - //IL_0162: Unknown result type (might be due to invalid IL or missing references) - //IL_0174: Unknown result type (might be due to invalid IL or missing references) - //IL_017d: Expected O, but got Unknown - //IL_0196: Unknown result type (might be due to invalid IL or missing references) - float num = RadioSize / 2f; - float num2 = ((SKRect)(ref bounds)).Left + num; - float midY = ((SKRect)(ref bounds)).MidY; - SKPaint val = new SKPaint - { - Color = ((!base.IsEnabled) ? DisabledColor : (IsChecked ? RadioColor : UncheckedColor)), - Style = (SKPaintStyle)1, - StrokeWidth = 2f, - IsAntialias = true - }; - try - { - canvas.DrawCircle(num2, midY, num - 1f, val); - if (IsChecked) - { - SKPaint val2 = new SKPaint - { - Color = (base.IsEnabled ? RadioColor : DisabledColor), - Style = (SKPaintStyle)0, - IsAntialias = true - }; - try - { - canvas.DrawCircle(num2, midY, num - 5f, val2); - } - finally - { - ((IDisposable)val2)?.Dispose(); - } - } - if (base.IsFocused) - { - SKPaint val3 = new SKPaint(); - SKColor radioColor = RadioColor; - val3.Color = ((SKColor)(ref radioColor)).WithAlpha((byte)80); - val3.Style = (SKPaintStyle)0; - val3.IsAntialias = true; - SKPaint val4 = val3; - try - { - canvas.DrawCircle(num2, midY, num + 4f, val4); - } - finally - { - ((IDisposable)val4)?.Dispose(); - } - } - if (string.IsNullOrEmpty(Content)) - { - return; - } - SKFont val5 = new SKFont(SKTypeface.Default, FontSize, 1f, 0f); - try - { - SKPaint val6 = new SKPaint(val5) - { - Color = (base.IsEnabled ? TextColor : DisabledColor), - IsAntialias = true - }; - try - { - float num3 = ((SKRect)(ref bounds)).Left + RadioSize + Spacing; - SKRect val7 = default(SKRect); - val6.MeasureText(Content, ref val7); - canvas.DrawText(Content, num3, ((SKRect)(ref bounds)).MidY - ((SKRect)(ref val7)).MidY, val6); - } - finally - { - ((IDisposable)val6)?.Dispose(); - } - } - finally - { - ((IDisposable)val5)?.Dispose(); - } - } - finally - { - ((IDisposable)val)?.Dispose(); - } - } + private void OnGroupNameChanged(string? oldValue, string? newValue) + { + RemoveFromGroup(oldValue); + AddToGroup(newValue); + } - public override void OnPointerPressed(PointerEventArgs e) - { - if (base.IsEnabled && !IsChecked) - { - IsChecked = true; - } - } + private void AddToGroup(string? groupName) + { + if (string.IsNullOrEmpty(groupName)) return; - public override void OnKeyDown(KeyEventArgs e) - { - if (base.IsEnabled && (e.Key == Key.Space || e.Key == Key.Enter)) - { - if (!IsChecked) - { - IsChecked = true; - } - e.Handled = true; - } - } + if (!_groups.TryGetValue(groupName, out var group)) + { + group = new List>(); + _groups[groupName] = group; + } - protected override void OnEnabledChanged() - { - base.OnEnabledChanged(); - SkiaVisualStateManager.GoToState(this, base.IsEnabled ? "Normal" : "Disabled"); - } + group.RemoveAll(wr => !wr.TryGetTarget(out _)); + group.Add(new WeakReference(this)); + } - protected override SKSize MeasureOverride(SKSize availableSize) - { - //IL_007e: Unknown result type (might be due to invalid IL or missing references) - //IL_0028: Unknown result type (might be due to invalid IL or missing references) - //IL_002e: Expected O, but got Unknown - //IL_002f: Unknown result type (might be due to invalid IL or missing references) - //IL_0035: Expected O, but got Unknown - float num = 0f; - if (!string.IsNullOrEmpty(Content)) - { - SKFont val = new SKFont(SKTypeface.Default, FontSize, 1f, 0f); - try - { - SKPaint val2 = new SKPaint(val); - try - { - num = val2.MeasureText(Content) + Spacing; - } - finally - { - ((IDisposable)val2)?.Dispose(); - } - } - finally - { - ((IDisposable)val)?.Dispose(); - } - } - return new SKSize(RadioSize + num, Math.Max(RadioSize, FontSize * 1.5f)); - } + private void RemoveFromGroup(string? groupName) + { + if (string.IsNullOrEmpty(groupName)) return; + + if (_groups.TryGetValue(groupName, out var group)) + { + group.RemoveAll(wr => !wr.TryGetTarget(out var target) || target == this); + if (group.Count == 0) _groups.Remove(groupName); + } + } + + private void UncheckOthersInGroup() + { + if (string.IsNullOrEmpty(GroupName)) return; + + if (_groups.TryGetValue(GroupName, out var group)) + { + foreach (var weakRef in group) + { + if (weakRef.TryGetTarget(out var radioButton) && radioButton != this && radioButton.IsChecked) + { + radioButton.SetValue(IsCheckedProperty, false); + } + } + } + } + + protected override void OnDraw(SKCanvas canvas, SKRect bounds) + { + var radioRadius = RadioSize / 2; + var radioCenterX = bounds.Left + radioRadius; + var radioCenterY = bounds.MidY; + + using var outerPaint = new SKPaint + { + Color = IsEnabled ? (IsChecked ? RadioColor : UncheckedColor) : DisabledColor, + Style = SKPaintStyle.Stroke, + StrokeWidth = 2, + IsAntialias = true + }; + canvas.DrawCircle(radioCenterX, radioCenterY, radioRadius - 1, outerPaint); + + if (IsChecked) + { + using var innerPaint = new SKPaint + { + Color = IsEnabled ? RadioColor : DisabledColor, + Style = SKPaintStyle.Fill, + IsAntialias = true + }; + canvas.DrawCircle(radioCenterX, radioCenterY, radioRadius - 5, innerPaint); + } + + if (IsFocused) + { + using var focusPaint = new SKPaint + { + Color = RadioColor.WithAlpha(80), + Style = SKPaintStyle.Fill, + IsAntialias = true + }; + canvas.DrawCircle(radioCenterX, radioCenterY, radioRadius + 4, focusPaint); + } + + if (!string.IsNullOrEmpty(Content)) + { + using var font = new SKFont(SKTypeface.Default, FontSize); + using var textPaint = new SKPaint(font) + { + Color = IsEnabled ? TextColor : DisabledColor, + IsAntialias = true + }; + + var textX = bounds.Left + RadioSize + Spacing; + var textBounds = new SKRect(); + textPaint.MeasureText(Content, ref textBounds); + canvas.DrawText(Content, textX, bounds.MidY - textBounds.MidY, textPaint); + } + } + + public override void OnPointerPressed(PointerEventArgs e) + { + if (!IsEnabled) return; + if (!IsChecked) IsChecked = true; + } + + public override void OnKeyDown(KeyEventArgs e) + { + if (!IsEnabled) return; + + if (e.Key == Key.Space || e.Key == Key.Enter) + { + if (!IsChecked) IsChecked = true; + e.Handled = true; + } + } + + protected override void OnEnabledChanged() + { + base.OnEnabledChanged(); + SkiaVisualStateManager.GoToState(this, IsEnabled ? SkiaVisualStateManager.CommonStates.Normal : SkiaVisualStateManager.CommonStates.Disabled); + } + + protected override SKSize MeasureOverride(SKSize availableSize) + { + var textWidth = 0f; + if (!string.IsNullOrEmpty(Content)) + { + using var font = new SKFont(SKTypeface.Default, FontSize); + using var paint = new SKPaint(font); + textWidth = paint.MeasureText(Content) + Spacing; + } + return new SKSize(RadioSize + textWidth, Math.Max(RadioSize, FontSize * 1.5f)); + } } diff --git a/Views/SkiaRefreshView.cs b/Views/SkiaRefreshView.cs index edff168..28c70d0 100644 --- a/Views/SkiaRefreshView.cs +++ b/Views/SkiaRefreshView.cs @@ -1,315 +1,278 @@ -using System; -using System.Windows.Input; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using SkiaSharp; namespace Microsoft.Maui.Platform; +/// +/// A pull-to-refresh container view. +/// public class SkiaRefreshView : SkiaLayoutView { - private SkiaView? _content; + private SkiaView? _content; + private bool _isRefreshing = false; + private float _pullDistance = 0f; + private float _refreshThreshold = 80f; + private bool _isPulling = false; + private float _pullStartY; + private float _spinnerRotation = 0f; + private DateTime _lastSpinnerUpdate; - private bool _isRefreshing; + /// + /// Gets or sets the content view. + /// + public SkiaView? Content + { + get => _content; + set + { + if (_content != value) + { + if (_content != null) + { + RemoveChild(_content); + } - private float _pullDistance; + _content = value; - private float _refreshThreshold = 80f; + if (_content != null) + { + AddChild(_content); + } - private bool _isPulling; + InvalidateMeasure(); + Invalidate(); + } + } + } - private float _pullStartY; + /// + /// Gets or sets whether the view is currently refreshing. + /// + public bool IsRefreshing + { + get => _isRefreshing; + set + { + if (_isRefreshing != value) + { + _isRefreshing = value; + if (!value) + { + _pullDistance = 0; + } + Invalidate(); + } + } + } - private float _spinnerRotation; + /// + /// Gets or sets the pull distance required to trigger refresh. + /// + public float RefreshThreshold + { + get => _refreshThreshold; + set => _refreshThreshold = Math.Max(40, value); + } - private DateTime _lastSpinnerUpdate; + /// + /// Gets or sets the refresh indicator color. + /// + public SKColor RefreshColor { get; set; } = new SKColor(33, 150, 243); - public SkiaView? Content - { - get - { - return _content; - } - set - { - if (_content != value) - { - if (_content != null) - { - RemoveChild(_content); - } - _content = value; - if (_content != null) - { - AddChild(_content); - } - InvalidateMeasure(); - Invalidate(); - } - } - } + /// + /// Gets or sets the background color of the refresh indicator. + /// + public SKColor RefreshBackgroundColor { get; set; } = SKColors.White; - public bool IsRefreshing - { - get - { - return _isRefreshing; - } - set - { - if (_isRefreshing != value) - { - _isRefreshing = value; - if (!value) - { - _pullDistance = 0f; - } - Invalidate(); - } - } - } + /// + /// Event raised when refresh is triggered. + /// + public event EventHandler? Refreshing; - public float RefreshThreshold - { - get - { - return _refreshThreshold; - } - set - { - _refreshThreshold = Math.Max(40f, value); - } - } + protected override SKSize MeasureOverride(SKSize availableSize) + { + if (_content != null) + { + _content.Measure(availableSize); + } + return availableSize; + } - public SKColor RefreshColor { get; set; } = new SKColor((byte)33, (byte)150, (byte)243); + protected override SKRect ArrangeOverride(SKRect bounds) + { + if (_content != null) + { + float offset = _isRefreshing ? _refreshThreshold : _pullDistance; + var contentBounds = new SKRect( + bounds.Left, + bounds.Top + offset, + bounds.Right, + bounds.Bottom + offset); + _content.Arrange(contentBounds); + } + return bounds; + } - public SKColor RefreshBackgroundColor { get; set; } = SKColors.White; + protected override void OnDraw(SKCanvas canvas, SKRect bounds) + { + canvas.Save(); + canvas.ClipRect(bounds); - public ICommand? Command { get; set; } + // Draw refresh indicator + float indicatorY = bounds.Top + (_isRefreshing ? _refreshThreshold : _pullDistance) / 2; - public object? CommandParameter { get; set; } + if (_pullDistance > 0 || _isRefreshing) + { + DrawRefreshIndicator(canvas, bounds.MidX, indicatorY); + } - public event EventHandler? Refreshing; + // Draw content + _content?.Draw(canvas); - protected override SKSize MeasureOverride(SKSize availableSize) - { - //IL_0015: Unknown result type (might be due to invalid IL or missing references) - //IL_000e: Unknown result type (might be due to invalid IL or missing references) - //IL_000f: Unknown result type (might be due to invalid IL or missing references) - if (_content != null) - { - _content.Measure(availableSize); - } - return availableSize; - } + canvas.Restore(); + } - protected override SKRect ArrangeOverride(SKRect bounds) - { - //IL_0052: Unknown result type (might be due to invalid IL or missing references) - //IL_004c: Unknown result type (might be due to invalid IL or missing references) - if (_content != null) - { - float num = (_isRefreshing ? _refreshThreshold : _pullDistance); - SKRect bounds2 = default(SKRect); - ((SKRect)(ref bounds2))._002Ector(((SKRect)(ref bounds)).Left, ((SKRect)(ref bounds)).Top + num, ((SKRect)(ref bounds)).Right, ((SKRect)(ref bounds)).Bottom + num); - _content.Arrange(bounds2); - } - return bounds; - } + private void DrawRefreshIndicator(SKCanvas canvas, float x, float y) + { + float size = 36f; + float progress = Math.Clamp(_pullDistance / _refreshThreshold, 0f, 1f); - protected override void OnDraw(SKCanvas canvas, SKRect bounds) - { - //IL_0008: Unknown result type (might be due to invalid IL or missing references) - canvas.Save(); - canvas.ClipRect(bounds, (SKClipOperation)1, false); - float y = ((SKRect)(ref bounds)).Top + (_isRefreshing ? _refreshThreshold : _pullDistance) / 2f; - if (_pullDistance > 0f || _isRefreshing) - { - DrawRefreshIndicator(canvas, ((SKRect)(ref bounds)).MidX, y); - } - _content?.Draw(canvas); - canvas.Restore(); - } + // Draw background circle + using var bgPaint = new SKPaint + { + Color = RefreshBackgroundColor, + Style = SKPaintStyle.Fill, + IsAntialias = true + }; - private void DrawRefreshIndicator(SKCanvas canvas, float x, float y) - { - //IL_0023: Unknown result type (might be due to invalid IL or missing references) - //IL_0028: Unknown result type (might be due to invalid IL or missing references) - //IL_002a: Unknown result type (might be due to invalid IL or missing references) - //IL_0034: Unknown result type (might be due to invalid IL or missing references) - //IL_003b: Unknown result type (might be due to invalid IL or missing references) - //IL_0043: Expected O, but got Unknown - //IL_005d: Unknown result type (might be due to invalid IL or missing references) - //IL_007c: Unknown result type (might be due to invalid IL or missing references) - //IL_0081: Unknown result type (might be due to invalid IL or missing references) - //IL_0083: Unknown result type (might be due to invalid IL or missing references) - //IL_008d: Unknown result type (might be due to invalid IL or missing references) - //IL_0094: Unknown result type (might be due to invalid IL or missing references) - //IL_009f: Unknown result type (might be due to invalid IL or missing references) - //IL_00a6: Unknown result type (might be due to invalid IL or missing references) - //IL_00ae: Expected O, but got Unknown - //IL_0185: Unknown result type (might be due to invalid IL or missing references) - //IL_018c: Expected O, but got Unknown - //IL_0111: Unknown result type (might be due to invalid IL or missing references) - //IL_0118: Expected O, but got Unknown - //IL_01aa: Unknown result type (might be due to invalid IL or missing references) - //IL_01af: Unknown result type (might be due to invalid IL or missing references) - //IL_01bc: Unknown result type (might be due to invalid IL or missing references) - //IL_0136: Unknown result type (might be due to invalid IL or missing references) - //IL_013b: Unknown result type (might be due to invalid IL or missing references) - //IL_013f: Unknown result type (might be due to invalid IL or missing references) - float num = 36f; - float num2 = Math.Clamp(_pullDistance / _refreshThreshold, 0f, 1f); - SKPaint val = new SKPaint - { - Color = RefreshBackgroundColor, - Style = (SKPaintStyle)0, - IsAntialias = true - }; - try - { - val.ImageFilter = SKImageFilter.CreateDropShadow(0f, 2f, 4f, 4f, new SKColor((byte)0, (byte)0, (byte)0, (byte)40)); - canvas.DrawCircle(x, y, num / 2f, val); - SKPaint val2 = new SKPaint - { - Color = RefreshColor, - Style = (SKPaintStyle)1, - StrokeWidth = 3f, - IsAntialias = true, - StrokeCap = (SKStrokeCap)1 - }; - try - { - if (_isRefreshing) - { - DateTime utcNow = DateTime.UtcNow; - float num3 = (float)(utcNow - _lastSpinnerUpdate).TotalMilliseconds; - _spinnerRotation += num3 * 0.36f; - _lastSpinnerUpdate = utcNow; - canvas.Save(); - canvas.Translate(x, y); - canvas.RotateDegrees(_spinnerRotation); - SKPath val3 = new SKPath(); - try - { - SKRect val4 = new SKRect((0f - num) / 3f, (0f - num) / 3f, num / 3f, num / 3f); - val3.AddArc(val4, 0f, 270f); - canvas.DrawPath(val3, val2); - canvas.Restore(); - Invalidate(); - return; - } - finally - { - ((IDisposable)val3)?.Dispose(); - } - } - canvas.Save(); - canvas.Translate(x, y); - SKPath val5 = new SKPath(); - try - { - SKRect val6 = new SKRect((0f - num) / 3f, (0f - num) / 3f, num / 3f, num / 3f); - float num4 = 270f * num2; - val5.AddArc(val6, -90f, num4); - canvas.DrawPath(val5, val2); - canvas.Restore(); - } - finally - { - ((IDisposable)val5)?.Dispose(); - } - } - finally - { - ((IDisposable)val2)?.Dispose(); - } - } - finally - { - ((IDisposable)val)?.Dispose(); - } - } + // Add shadow + bgPaint.ImageFilter = SKImageFilter.CreateDropShadow(0, 2, 4, 4, new SKColor(0, 0, 0, 40)); + canvas.DrawCircle(x, y, size / 2, bgPaint); - public override SkiaView? HitTest(float x, float y) - { - //IL_0009: Unknown result type (might be due to invalid IL or missing references) - //IL_000e: Unknown result type (might be due to invalid IL or missing references) - if (base.IsVisible) - { - SKRect bounds = base.Bounds; - if (((SKRect)(ref bounds)).Contains(x, y)) - { - if (_content != null) - { - SkiaView skiaView = _content.HitTest(x, y); - if (skiaView != null) - { - return skiaView; - } - } - return this; - } - } - return null; - } + // Draw spinner + using var spinnerPaint = new SKPaint + { + Color = RefreshColor, + Style = SKPaintStyle.Stroke, + StrokeWidth = 3, + IsAntialias = true, + StrokeCap = SKStrokeCap.Round + }; - public override void OnPointerPressed(PointerEventArgs e) - { - if (base.IsEnabled && !_isRefreshing) - { - bool flag = true; - if (_content is SkiaScrollView skiaScrollView) - { - flag = skiaScrollView.ScrollY <= 0f; - } - if (flag) - { - _isPulling = true; - _pullStartY = e.Y; - _pullDistance = 0f; - } - base.OnPointerPressed(e); - } - } + if (_isRefreshing) + { + // Animate spinner + var now = DateTime.UtcNow; + float elapsed = (float)(now - _lastSpinnerUpdate).TotalMilliseconds; + _spinnerRotation += elapsed * 0.36f; // 360 degrees per second + _lastSpinnerUpdate = now; - public override void OnPointerMoved(PointerEventArgs e) - { - if (_isPulling) - { - float num = e.Y - _pullStartY; - if (num > 0f) - { - _pullDistance = num * 0.5f; - _pullDistance = Math.Min(_pullDistance, _refreshThreshold * 1.5f); - Invalidate(); - e.Handled = true; - } - else - { - _pullDistance = 0f; - } - base.OnPointerMoved(e); - } - } + canvas.Save(); + canvas.Translate(x, y); + canvas.RotateDegrees(_spinnerRotation); - public override void OnPointerReleased(PointerEventArgs e) - { - if (_isPulling) - { - _isPulling = false; - if (_pullDistance >= _refreshThreshold) - { - _isRefreshing = true; - _pullDistance = _refreshThreshold; - _lastSpinnerUpdate = DateTime.UtcNow; - this.Refreshing?.Invoke(this, EventArgs.Empty); - Command?.Execute(CommandParameter); - } - else - { - _pullDistance = 0f; - } - Invalidate(); - base.OnPointerReleased(e); - } - } + // Draw spinning arc + using var path = new SKPath(); + var rect = new SKRect(-size / 3, -size / 3, size / 3, size / 3); + path.AddArc(rect, 0, 270); + canvas.DrawPath(path, spinnerPaint); + + canvas.Restore(); + + Invalidate(); // Continue animation + } + else + { + // Draw progress arc + canvas.Save(); + canvas.Translate(x, y); + + using var path = new SKPath(); + var rect = new SKRect(-size / 3, -size / 3, size / 3, size / 3); + float sweepAngle = 270 * progress; + path.AddArc(rect, -90, sweepAngle); + canvas.DrawPath(path, spinnerPaint); + + canvas.Restore(); + } + } + + public override SkiaView? HitTest(float x, float y) + { + if (!IsVisible || !Bounds.Contains(x, y)) return null; + + if (_content != null) + { + var hit = _content.HitTest(x, y); + if (hit != null) return hit; + } + + return this; + } + + public override void OnPointerPressed(PointerEventArgs e) + { + if (!IsEnabled || _isRefreshing) return; + + // Check if content is at top (can pull to refresh) + bool canPull = true; + if (_content is SkiaScrollView scrollView) + { + canPull = scrollView.ScrollY <= 0; + } + + if (canPull) + { + _isPulling = true; + _pullStartY = e.Y; + _pullDistance = 0; + } + + base.OnPointerPressed(e); + } + + public override void OnPointerMoved(PointerEventArgs e) + { + if (!_isPulling) return; + + float delta = e.Y - _pullStartY; + if (delta > 0) + { + // Apply resistance + _pullDistance = delta * 0.5f; + _pullDistance = Math.Min(_pullDistance, _refreshThreshold * 1.5f); + Invalidate(); + e.Handled = true; + } + else + { + _pullDistance = 0; + } + + base.OnPointerMoved(e); + } + + public override void OnPointerReleased(PointerEventArgs e) + { + if (!_isPulling) return; + + _isPulling = false; + + if (_pullDistance >= _refreshThreshold) + { + _isRefreshing = true; + _pullDistance = _refreshThreshold; + _lastSpinnerUpdate = DateTime.UtcNow; + Refreshing?.Invoke(this, EventArgs.Empty); + } + else + { + _pullDistance = 0; + } + + Invalidate(); + base.OnPointerReleased(e); + } } diff --git a/Views/SkiaScrollView.cs b/Views/SkiaScrollView.cs index 710641d..6f412a7 100644 --- a/Views/SkiaScrollView.cs +++ b/Views/SkiaScrollView.cs @@ -1,899 +1,784 @@ -using System; -using Microsoft.Maui.Controls; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using SkiaSharp; namespace Microsoft.Maui.Platform; +/// +/// Skia-rendered scroll view container with full XAML styling support. +/// public class SkiaScrollView : SkiaView { - public static readonly BindableProperty OrientationProperty = BindableProperty.Create("Orientation", typeof(ScrollOrientation), typeof(SkiaScrollView), (object)ScrollOrientation.Both, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaScrollView)(object)b).InvalidateMeasure(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + #region BindableProperties - public static readonly BindableProperty HorizontalScrollBarVisibilityProperty = BindableProperty.Create("HorizontalScrollBarVisibility", typeof(ScrollBarVisibility), typeof(SkiaScrollView), (object)ScrollBarVisibility.Auto, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaScrollView)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for Orientation. + /// + public static readonly BindableProperty OrientationProperty = + BindableProperty.Create( + nameof(Orientation), + typeof(ScrollOrientation), + typeof(SkiaScrollView), + ScrollOrientation.Both, + propertyChanged: (b, o, n) => ((SkiaScrollView)b).InvalidateMeasure()); - public static readonly BindableProperty VerticalScrollBarVisibilityProperty = BindableProperty.Create("VerticalScrollBarVisibility", typeof(ScrollBarVisibility), typeof(SkiaScrollView), (object)ScrollBarVisibility.Auto, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaScrollView)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for HorizontalScrollBarVisibility. + /// + public static readonly BindableProperty HorizontalScrollBarVisibilityProperty = + BindableProperty.Create( + nameof(HorizontalScrollBarVisibility), + typeof(ScrollBarVisibility), + typeof(SkiaScrollView), + ScrollBarVisibility.Auto, + propertyChanged: (b, o, n) => ((SkiaScrollView)b).Invalidate()); - public static readonly BindableProperty ScrollBarColorProperty = BindableProperty.Create("ScrollBarColor", typeof(SKColor), typeof(SkiaScrollView), (object)new SKColor((byte)128, (byte)128, (byte)128, (byte)128), (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaScrollView)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for VerticalScrollBarVisibility. + /// + public static readonly BindableProperty VerticalScrollBarVisibilityProperty = + BindableProperty.Create( + nameof(VerticalScrollBarVisibility), + typeof(ScrollBarVisibility), + typeof(SkiaScrollView), + ScrollBarVisibility.Auto, + propertyChanged: (b, o, n) => ((SkiaScrollView)b).Invalidate()); - public static readonly BindableProperty ScrollBarWidthProperty = BindableProperty.Create("ScrollBarWidth", typeof(float), typeof(SkiaScrollView), (object)8f, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaScrollView)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for ScrollBarColor. + /// + public static readonly BindableProperty ScrollBarColorProperty = + BindableProperty.Create( + nameof(ScrollBarColor), + typeof(SKColor), + typeof(SkiaScrollView), + new SKColor(0x80, 0x80, 0x80, 0x80), + propertyChanged: (b, o, n) => ((SkiaScrollView)b).Invalidate()); - private SkiaView? _content; + /// + /// Bindable property for ScrollBarWidth. + /// + public static readonly BindableProperty ScrollBarWidthProperty = + BindableProperty.Create( + nameof(ScrollBarWidth), + typeof(float), + typeof(SkiaScrollView), + 8f, + propertyChanged: (b, o, n) => ((SkiaScrollView)b).Invalidate()); - private float _scrollX; + #endregion - private float _scrollY; + #region Properties - private float _velocityX; + /// + /// Gets or sets the scroll orientation. + /// + public ScrollOrientation Orientation + { + get => (ScrollOrientation)GetValue(OrientationProperty); + set => SetValue(OrientationProperty, value); + } - private float _velocityY; + /// + /// Gets or sets whether to show horizontal scrollbar. + /// + public ScrollBarVisibility HorizontalScrollBarVisibility + { + get => (ScrollBarVisibility)GetValue(HorizontalScrollBarVisibilityProperty); + set => SetValue(HorizontalScrollBarVisibilityProperty, value); + } - private bool _isDragging; + /// + /// Gets or sets whether to show vertical scrollbar. + /// + public ScrollBarVisibility VerticalScrollBarVisibility + { + get => (ScrollBarVisibility)GetValue(VerticalScrollBarVisibilityProperty); + set => SetValue(VerticalScrollBarVisibilityProperty, value); + } - private bool _isDraggingVerticalScrollbar; + /// + /// Scrollbar color. + /// + public SKColor ScrollBarColor + { + get => (SKColor)GetValue(ScrollBarColorProperty); + set => SetValue(ScrollBarColorProperty, value); + } - private bool _isDraggingHorizontalScrollbar; + /// + /// Scrollbar width. + /// + public float ScrollBarWidth + { + get => (float)GetValue(ScrollBarWidthProperty); + set => SetValue(ScrollBarWidthProperty, value); + } - private float _scrollbarDragStartY; + #endregion - private float _scrollbarDragStartScrollY; + private SkiaView? _content; + private float _scrollX; + private float _scrollY; + private float _velocityX; + private float _velocityY; + private bool _isDragging; + private bool _isDraggingVerticalScrollbar; + private bool _isDraggingHorizontalScrollbar; + private float _scrollbarDragStartY; + private float _scrollbarDragStartScrollY; + private float _scrollbarDragStartX; + private float _scrollbarDragStartScrollX; + private float _scrollbarDragAvailableTrack; // Cache to prevent stutter + private float _scrollbarDragScrollableExtent; // Cache to prevent stutter + private float _lastPointerX; + private float _lastPointerY; - private float _scrollbarDragStartX; + /// + /// Gets or sets the content view. + /// + public SkiaView? Content + { + get => _content; + set + { + if (_content != value) + { + if (_content != null) + _content.Parent = null; - private float _scrollbarDragStartScrollX; + _content = value; - private float _scrollbarDragAvailableTrack; + if (_content != null) + { + _content.Parent = this; - private float _scrollbarDragScrollableExtent; + // Propagate binding context to new content + if (BindingContext != null) + { + SetInheritedBindingContext(_content, BindingContext); + } + } - private float _lastPointerX; + InvalidateMeasure(); + Invalidate(); + } + } + } - private float _lastPointerY; + /// + /// Called when binding context changes. Propagates to content. + /// + protected override void OnBindingContextChanged() + { + base.OnBindingContextChanged(); - public ScrollOrientation Orientation - { - get - { - return (ScrollOrientation)((BindableObject)this).GetValue(OrientationProperty); - } - set - { - ((BindableObject)this).SetValue(OrientationProperty, (object)value); - } - } + // Propagate binding context to content + if (_content != null) + { + SetInheritedBindingContext(_content, BindingContext); + } + } - public ScrollBarVisibility HorizontalScrollBarVisibility - { - get - { - return (ScrollBarVisibility)((BindableObject)this).GetValue(HorizontalScrollBarVisibilityProperty); - } - set - { - ((BindableObject)this).SetValue(HorizontalScrollBarVisibilityProperty, (object)value); - } - } + /// + /// Gets or sets the horizontal scroll position. + /// + public float ScrollX + { + get => _scrollX; + set + { + var clamped = ClampScrollX(value); + if (_scrollX != clamped) + { + _scrollX = clamped; + Scrolled?.Invoke(this, new ScrolledEventArgs(_scrollX, _scrollY)); + Invalidate(); + } + } + } - public ScrollBarVisibility VerticalScrollBarVisibility - { - get - { - return (ScrollBarVisibility)((BindableObject)this).GetValue(VerticalScrollBarVisibilityProperty); - } - set - { - ((BindableObject)this).SetValue(VerticalScrollBarVisibilityProperty, (object)value); - } - } + /// + /// Gets or sets the vertical scroll position. + /// + public float ScrollY + { + get => _scrollY; + set + { + var clamped = ClampScrollY(value); + if (_scrollY != clamped) + { + _scrollY = clamped; + Scrolled?.Invoke(this, new ScrolledEventArgs(_scrollX, _scrollY)); + Invalidate(); + } + } + } - public SKColor ScrollBarColor - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKColor)((BindableObject)this).GetValue(ScrollBarColorProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(ScrollBarColorProperty, (object)value); - } - } + /// + /// Gets the maximum horizontal scroll extent. + /// + public float ScrollableWidth + { + get + { + // Handle infinite or NaN bounds - use a reasonable default viewport + var viewportWidth = float.IsInfinity(Bounds.Width) || float.IsNaN(Bounds.Width) || Bounds.Width <= 0 + ? 800f + : Bounds.Width; + return Math.Max(0, ContentSize.Width - viewportWidth); + } + } - public float ScrollBarWidth - { - get - { - return (float)((BindableObject)this).GetValue(ScrollBarWidthProperty); - } - set - { - ((BindableObject)this).SetValue(ScrollBarWidthProperty, (object)value); - } - } + /// + /// Gets the maximum vertical scroll extent. + /// + public float ScrollableHeight + { + get + { + // Handle infinite, NaN, or unreasonably large bounds - use a reasonable default viewport + var boundsHeight = Bounds.Height; + var viewportHeight = (float.IsInfinity(boundsHeight) || float.IsNaN(boundsHeight) || boundsHeight <= 0 || boundsHeight > 10000) + ? 544f // Default viewport height (600 - 56 for shell header) + : boundsHeight; + return Math.Max(0, ContentSize.Height - viewportHeight); + } + } - public SkiaView? Content - { - get - { - return _content; - } - set - { - if (_content == value) - { - return; - } - if (_content != null) - { - _content.Parent = null; - } - _content = value; - if (_content != null) - { - _content.Parent = this; - if (((BindableObject)this).BindingContext != null) - { - BindableObject.SetInheritedBindingContext((BindableObject)(object)_content, ((BindableObject)this).BindingContext); - } - } - InvalidateMeasure(); - Invalidate(); - } - } + /// + /// Gets the content size. + /// + public SKSize ContentSize { get; private set; } - public float ScrollX - { - get - { - return _scrollX; - } - set - { - float num = ClampScrollX(value); - if (_scrollX != num) - { - _scrollX = num; - this.Scrolled?.Invoke(this, new ScrolledEventArgs(_scrollX, _scrollY)); - Invalidate(); - } - } - } + /// + /// Event raised when scroll position changes. + /// + public event EventHandler? Scrolled; - public float ScrollY - { - get - { - return _scrollY; - } - set - { - float num = ClampScrollY(value); - if (_scrollY != num) - { - _scrollY = num; - this.Scrolled?.Invoke(this, new ScrolledEventArgs(_scrollX, _scrollY)); - Invalidate(); - } - } - } + protected override void OnDraw(SKCanvas canvas, SKRect bounds) + { + // Clip to bounds + canvas.Save(); + canvas.ClipRect(bounds); - public float ScrollableWidth - { - get - { - //IL_0001: Unknown result type (might be due to invalid IL or missing references) - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - //IL_0016: Unknown result type (might be due to invalid IL or missing references) - //IL_001b: Unknown result type (might be due to invalid IL or missing references) - //IL_005b: Unknown result type (might be due to invalid IL or missing references) - //IL_0060: Unknown result type (might be due to invalid IL or missing references) - //IL_002b: Unknown result type (might be due to invalid IL or missing references) - //IL_0030: Unknown result type (might be due to invalid IL or missing references) - //IL_0040: Unknown result type (might be due to invalid IL or missing references) - //IL_0045: Unknown result type (might be due to invalid IL or missing references) - SKRect bounds = base.Bounds; - float num; - if (!float.IsInfinity(((SKRect)(ref bounds)).Width)) - { - bounds = base.Bounds; - if (!float.IsNaN(((SKRect)(ref bounds)).Width)) - { - bounds = base.Bounds; - if (!(((SKRect)(ref bounds)).Width <= 0f)) - { - bounds = base.Bounds; - num = ((SKRect)(ref bounds)).Width; - goto IL_0054; - } - } - } - num = 800f; - goto IL_0054; - IL_0054: - float num2 = num; - SKSize contentSize = ContentSize; - return Math.Max(0f, ((SKSize)(ref contentSize)).Width - num2); - } - } + // Draw content with scroll offset + if (_content != null) + { + // Ensure content is measured and arranged + // Account for vertical scrollbar width to prevent horizontal scrollbar from appearing + var effectiveWidth = bounds.Width; + if (Orientation != ScrollOrientation.Horizontal && VerticalScrollBarVisibility != ScrollBarVisibility.Never) + { + // Reserve space for vertical scrollbar if content might be taller than viewport + effectiveWidth -= ScrollBarWidth; + } + var availableSize = new SKSize(effectiveWidth, float.PositiveInfinity); + // Update ContentSize with the properly constrained measurement + ContentSize = _content.Measure(availableSize); - public float ScrollableHeight - { - get - { - //IL_0001: Unknown result type (might be due to invalid IL or missing references) - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - //IL_003e: Unknown result type (might be due to invalid IL or missing references) - //IL_0043: Unknown result type (might be due to invalid IL or missing references) - SKRect bounds = base.Bounds; - float height = ((SKRect)(ref bounds)).Height; - float num = ((float.IsInfinity(height) || float.IsNaN(height) || height <= 0f || height > 10000f) ? 544f : height); - SKSize contentSize = ContentSize; - return Math.Max(0f, ((SKSize)(ref contentSize)).Height - num); - } - } + // Apply content's margin + var margin = _content.Margin; + var contentBounds = new SKRect( + bounds.Left + (float)margin.Left, + bounds.Top + (float)margin.Top, + bounds.Left + Math.Max(bounds.Width, _content.DesiredSize.Width) - (float)margin.Right, + bounds.Top + Math.Max(bounds.Height, _content.DesiredSize.Height) - (float)margin.Bottom); + _content.Arrange(contentBounds); - public SKSize ContentSize { get; private set; } + canvas.Save(); + canvas.Translate(-_scrollX, -_scrollY); + _content.Draw(canvas); + canvas.Restore(); + } - public event EventHandler? Scrolled; + // Draw scrollbars + DrawScrollbars(canvas, bounds); - protected override void OnBindingContextChanged() - { - base.OnBindingContextChanged(); - if (_content != null) - { - BindableObject.SetInheritedBindingContext((BindableObject)(object)_content, ((BindableObject)this).BindingContext); - } - } + canvas.Restore(); + } - protected override void OnDraw(SKCanvas canvas, SKRect bounds) - { - //IL_0008: Unknown result type (might be due to invalid IL or missing references) - //IL_012d: Unknown result type (might be due to invalid IL or missing references) - //IL_0052: Unknown result type (might be due to invalid IL or missing references) - //IL_0053: Unknown result type (might be due to invalid IL or missing references) - //IL_0063: Unknown result type (might be due to invalid IL or missing references) - //IL_0068: Unknown result type (might be due to invalid IL or missing references) - //IL_009f: Unknown result type (might be due to invalid IL or missing references) - //IL_00a4: Unknown result type (might be due to invalid IL or missing references) - //IL_00d0: Unknown result type (might be due to invalid IL or missing references) - //IL_00d5: Unknown result type (might be due to invalid IL or missing references) - //IL_00f8: Unknown result type (might be due to invalid IL or missing references) - canvas.Save(); - canvas.ClipRect(bounds, (SKClipOperation)1, false); - if (_content != null) - { - float num = ((SKRect)(ref bounds)).Width; - if (Orientation != ScrollOrientation.Horizontal && VerticalScrollBarVisibility != ScrollBarVisibility.Never) - { - num -= ScrollBarWidth; - } - SKSize availableSize = default(SKSize); - ((SKSize)(ref availableSize))._002Ector(num, float.PositiveInfinity); - ContentSize = _content.Measure(availableSize); - Thickness margin = _content.Margin; - float num2 = ((SKRect)(ref bounds)).Left + (float)((Thickness)(ref margin)).Left; - float num3 = ((SKRect)(ref bounds)).Top + (float)((Thickness)(ref margin)).Top; - float left = ((SKRect)(ref bounds)).Left; - float width = ((SKRect)(ref bounds)).Width; - SKSize desiredSize = _content.DesiredSize; - float num4 = left + Math.Max(width, ((SKSize)(ref desiredSize)).Width) - (float)((Thickness)(ref margin)).Right; - float top = ((SKRect)(ref bounds)).Top; - float height = ((SKRect)(ref bounds)).Height; - desiredSize = _content.DesiredSize; - SKRect bounds2 = default(SKRect); - ((SKRect)(ref bounds2))._002Ector(num2, num3, num4, top + Math.Max(height, ((SKSize)(ref desiredSize)).Height) - (float)((Thickness)(ref margin)).Bottom); - _content.Arrange(bounds2); - canvas.Save(); - canvas.Translate(0f - _scrollX, 0f - _scrollY); - _content.Draw(canvas); - canvas.Restore(); - } - DrawScrollbars(canvas, bounds); - canvas.Restore(); - } + private void DrawScrollbars(SKCanvas canvas, SKRect bounds) + { + var showVertical = ShouldShowVerticalScrollbar(); + var showHorizontal = ShouldShowHorizontalScrollbar(); - private void DrawScrollbars(SKCanvas canvas, SKRect bounds) - { - //IL_0020: Unknown result type (might be due to invalid IL or missing references) - //IL_0039: Unknown result type (might be due to invalid IL or missing references) - bool flag = ShouldShowVerticalScrollbar(); - bool flag2 = ShouldShowHorizontalScrollbar(); - if (flag && ScrollableHeight > 0f) - { - DrawVerticalScrollbar(canvas, bounds, flag2); - } - if (flag2 && ScrollableWidth > 0f) - { - DrawHorizontalScrollbar(canvas, bounds, flag); - } - } + if (showVertical && ScrollableHeight > 0) + { + DrawVerticalScrollbar(canvas, bounds, showHorizontal); + } - private bool ShouldShowVerticalScrollbar() - { - if (Orientation == ScrollOrientation.Horizontal) - { - return false; - } - return VerticalScrollBarVisibility switch - { - ScrollBarVisibility.Always => true, - ScrollBarVisibility.Never => false, - _ => ScrollableHeight > 0f, - }; - } + if (showHorizontal && ScrollableWidth > 0) + { + DrawHorizontalScrollbar(canvas, bounds, showVertical); + } + } - private bool ShouldShowHorizontalScrollbar() - { - if (Orientation == ScrollOrientation.Vertical) - { - return false; - } - return HorizontalScrollBarVisibility switch - { - ScrollBarVisibility.Always => true, - ScrollBarVisibility.Never => false, - _ => ScrollableWidth > 0f, - }; - } + private bool ShouldShowVerticalScrollbar() + { + if (Orientation == ScrollOrientation.Horizontal) return false; - private void DrawVerticalScrollbar(SKCanvas canvas, SKRect bounds, bool hasHorizontal) - { - //IL_0026: Unknown result type (might be due to invalid IL or missing references) - //IL_002b: Unknown result type (might be due to invalid IL or missing references) - //IL_004f: Unknown result type (might be due to invalid IL or missing references) - //IL_0054: Unknown result type (might be due to invalid IL or missing references) - //IL_0056: Unknown result type (might be due to invalid IL or missing references) - //IL_0060: Unknown result type (might be due to invalid IL or missing references) - //IL_0068: Expected O, but got Unknown - //IL_0091: Unknown result type (might be due to invalid IL or missing references) - //IL_00a2: Unknown result type (might be due to invalid IL or missing references) - //IL_00a9: Expected O, but got Unknown - float num = ((SKRect)(ref bounds)).Height - (hasHorizontal ? ScrollBarWidth : 0f); - float height = ((SKRect)(ref bounds)).Height; - SKSize contentSize = ContentSize; - float num2 = Math.Max(20f, height / ((SKSize)(ref contentSize)).Height * num); - float num3 = ScrollY / ScrollableHeight * (num - num2); - SKPaint val = new SKPaint - { - Color = ScrollBarColor, - IsAntialias = true - }; - try - { - SKRoundRect val2 = new SKRoundRect(new SKRect(((SKRect)(ref bounds)).Right - ScrollBarWidth, ((SKRect)(ref bounds)).Top + num3, ((SKRect)(ref bounds)).Right, ((SKRect)(ref bounds)).Top + num3 + num2), ScrollBarWidth / 2f); - canvas.DrawRoundRect(val2, val); - } - finally - { - ((IDisposable)val)?.Dispose(); - } - } + return VerticalScrollBarVisibility switch + { + ScrollBarVisibility.Always => true, + ScrollBarVisibility.Never => false, + _ => ScrollableHeight > 0 + }; + } - private void DrawHorizontalScrollbar(SKCanvas canvas, SKRect bounds, bool hasVertical) - { - //IL_0026: Unknown result type (might be due to invalid IL or missing references) - //IL_002b: Unknown result type (might be due to invalid IL or missing references) - //IL_004f: Unknown result type (might be due to invalid IL or missing references) - //IL_0054: Unknown result type (might be due to invalid IL or missing references) - //IL_0056: Unknown result type (might be due to invalid IL or missing references) - //IL_0060: Unknown result type (might be due to invalid IL or missing references) - //IL_0068: Expected O, but got Unknown - //IL_0091: Unknown result type (might be due to invalid IL or missing references) - //IL_00a2: Unknown result type (might be due to invalid IL or missing references) - //IL_00a9: Expected O, but got Unknown - float num = ((SKRect)(ref bounds)).Width - (hasVertical ? ScrollBarWidth : 0f); - float width = ((SKRect)(ref bounds)).Width; - SKSize contentSize = ContentSize; - float num2 = Math.Max(20f, width / ((SKSize)(ref contentSize)).Width * num); - float num3 = ScrollX / ScrollableWidth * (num - num2); - SKPaint val = new SKPaint - { - Color = ScrollBarColor, - IsAntialias = true - }; - try - { - SKRoundRect val2 = new SKRoundRect(new SKRect(((SKRect)(ref bounds)).Left + num3, ((SKRect)(ref bounds)).Bottom - ScrollBarWidth, ((SKRect)(ref bounds)).Left + num3 + num2, ((SKRect)(ref bounds)).Bottom), ScrollBarWidth / 2f); - canvas.DrawRoundRect(val2, val); - } - finally - { - ((IDisposable)val)?.Dispose(); - } - } + private bool ShouldShowHorizontalScrollbar() + { + if (Orientation == ScrollOrientation.Vertical) return false; - public override void OnScroll(ScrollEventArgs e) - { - float num = 40f; - bool flag = false; - if (Orientation != ScrollOrientation.Horizontal && ScrollableHeight > 0f) - { - float scrollY = _scrollY; - ScrollY += e.DeltaY * num; - if (_scrollY != scrollY) - { - flag = true; - } - } - if (Orientation != ScrollOrientation.Vertical && ScrollableWidth > 0f) - { - float scrollX = _scrollX; - ScrollX += e.DeltaX * num; - if (_scrollX != scrollX) - { - flag = true; - } - } - if (flag) - { - e.Handled = true; - } - } + return HorizontalScrollBarVisibility switch + { + ScrollBarVisibility.Always => true, + ScrollBarVisibility.Never => false, + _ => ScrollableWidth > 0 + }; + } - public override void OnPointerPressed(PointerEventArgs e) - { - //IL_001c: Unknown result type (might be due to invalid IL or missing references) - //IL_0021: Unknown result type (might be due to invalid IL or missing references) - //IL_00df: Unknown result type (might be due to invalid IL or missing references) - //IL_00e4: Unknown result type (might be due to invalid IL or missing references) - //IL_0061: Unknown result type (might be due to invalid IL or missing references) - //IL_0066: Unknown result type (might be due to invalid IL or missing references) - //IL_0126: Unknown result type (might be due to invalid IL or missing references) - //IL_012b: Unknown result type (might be due to invalid IL or missing references) - //IL_0087: Unknown result type (might be due to invalid IL or missing references) - //IL_008c: Unknown result type (might be due to invalid IL or missing references) - //IL_0096: Unknown result type (might be due to invalid IL or missing references) - //IL_009b: Unknown result type (might be due to invalid IL or missing references) - //IL_014e: Unknown result type (might be due to invalid IL or missing references) - //IL_0153: Unknown result type (might be due to invalid IL or missing references) - //IL_015d: Unknown result type (might be due to invalid IL or missing references) - //IL_0162: Unknown result type (might be due to invalid IL or missing references) - SKRect bounds; - SKSize contentSize; - if (ShouldShowVerticalScrollbar() && ScrollableHeight > 0f) - { - SKRect verticalScrollbarThumbBounds = GetVerticalScrollbarThumbBounds(); - if (((SKRect)(ref verticalScrollbarThumbBounds)).Contains(e.X, e.Y)) - { - _isDraggingVerticalScrollbar = true; - _scrollbarDragStartY = e.Y; - _scrollbarDragStartScrollY = _scrollY; - bool flag = ShouldShowHorizontalScrollbar(); - bounds = base.Bounds; - float num = ((SKRect)(ref bounds)).Height - (flag ? ScrollBarWidth : 0f); - bounds = base.Bounds; - float height = ((SKRect)(ref bounds)).Height; - contentSize = ContentSize; - float num2 = Math.Max(20f, height / ((SKSize)(ref contentSize)).Height * num); - _scrollbarDragAvailableTrack = num - num2; - _scrollbarDragScrollableExtent = ScrollableHeight; - return; - } - } - if (ShouldShowHorizontalScrollbar() && ScrollableWidth > 0f) - { - SKRect horizontalScrollbarThumbBounds = GetHorizontalScrollbarThumbBounds(); - if (((SKRect)(ref horizontalScrollbarThumbBounds)).Contains(e.X, e.Y)) - { - _isDraggingHorizontalScrollbar = true; - _scrollbarDragStartX = e.X; - _scrollbarDragStartScrollX = _scrollX; - bool flag2 = ShouldShowVerticalScrollbar(); - bounds = base.Bounds; - float num3 = ((SKRect)(ref bounds)).Width - (flag2 ? ScrollBarWidth : 0f); - bounds = base.Bounds; - float width = ((SKRect)(ref bounds)).Width; - contentSize = ContentSize; - float num4 = Math.Max(20f, width / ((SKSize)(ref contentSize)).Width * num3); - _scrollbarDragAvailableTrack = num3 - num4; - _scrollbarDragScrollableExtent = ScrollableWidth; - return; - } - } - if (_content != null) - { - PointerEventArgs e2 = new PointerEventArgs(e.X + _scrollX, e.Y + _scrollY, e.Button); - SkiaView skiaView = _content.HitTest(e2.X, e2.Y); - if (skiaView != null && skiaView != _content) - { - skiaView.OnPointerPressed(e2); - return; - } - } - _isDragging = true; - _lastPointerX = e.X; - _lastPointerY = e.Y; - _velocityX = 0f; - _velocityY = 0f; - } + private void DrawVerticalScrollbar(SKCanvas canvas, SKRect bounds, bool hasHorizontal) + { + var trackHeight = bounds.Height - (hasHorizontal ? ScrollBarWidth : 0); + var thumbHeight = Math.Max(20, (bounds.Height / ContentSize.Height) * trackHeight); + var thumbY = (ScrollY / ScrollableHeight) * (trackHeight - thumbHeight); - public override void OnPointerMoved(PointerEventArgs e) - { - if (_isDraggingVerticalScrollbar) - { - if (_scrollbarDragAvailableTrack > 0f) - { - float num = (e.Y - _scrollbarDragStartY) / _scrollbarDragAvailableTrack * _scrollbarDragScrollableExtent; - ScrollY = _scrollbarDragStartScrollY + num; - } - } - else if (_isDraggingHorizontalScrollbar) - { - if (_scrollbarDragAvailableTrack > 0f) - { - float num2 = (e.X - _scrollbarDragStartX) / _scrollbarDragAvailableTrack * _scrollbarDragScrollableExtent; - ScrollX = _scrollbarDragStartScrollX + num2; - } - } - else if (_isDragging) - { - float num3 = _lastPointerX - e.X; - float num4 = _lastPointerY - e.Y; - _velocityX = num3; - _velocityY = num4; - if (Orientation != ScrollOrientation.Horizontal) - { - ScrollY += num4; - } - if (Orientation != ScrollOrientation.Vertical) - { - ScrollX += num3; - } - _lastPointerX = e.X; - _lastPointerY = e.Y; - } - } + using var paint = new SKPaint + { + Color = ScrollBarColor, + IsAntialias = true + }; - public override void OnPointerReleased(PointerEventArgs e) - { - _isDragging = false; - _isDraggingVerticalScrollbar = false; - _isDraggingHorizontalScrollbar = false; - } + var thumbRect = new SKRoundRect( + new SKRect( + bounds.Right - ScrollBarWidth, + bounds.Top + thumbY, + bounds.Right, + bounds.Top + thumbY + thumbHeight), + ScrollBarWidth / 2); - private SKRect GetVerticalScrollbarThumbBounds() - { - //IL_0008: Unknown result type (might be due to invalid IL or missing references) - //IL_000d: Unknown result type (might be due to invalid IL or missing references) - //IL_002e: Unknown result type (might be due to invalid IL or missing references) - //IL_0033: Unknown result type (might be due to invalid IL or missing references) - //IL_003d: Unknown result type (might be due to invalid IL or missing references) - //IL_0042: Unknown result type (might be due to invalid IL or missing references) - //IL_007b: Unknown result type (might be due to invalid IL or missing references) - //IL_0080: Unknown result type (might be due to invalid IL or missing references) - //IL_0091: Unknown result type (might be due to invalid IL or missing references) - //IL_0096: Unknown result type (might be due to invalid IL or missing references) - //IL_00a2: Unknown result type (might be due to invalid IL or missing references) - //IL_00a7: Unknown result type (might be due to invalid IL or missing references) - //IL_00b1: Unknown result type (might be due to invalid IL or missing references) - //IL_00b6: Unknown result type (might be due to invalid IL or missing references) - //IL_00c3: Unknown result type (might be due to invalid IL or missing references) - bool flag = ShouldShowHorizontalScrollbar(); - SKRect bounds = base.Bounds; - float num = ((SKRect)(ref bounds)).Height - (flag ? ScrollBarWidth : 0f); - bounds = base.Bounds; - float height = ((SKRect)(ref bounds)).Height; - SKSize contentSize = ContentSize; - float num2 = Math.Max(20f, height / ((SKSize)(ref contentSize)).Height * num); - float num3 = ((ScrollableHeight > 0f) ? (ScrollY / ScrollableHeight * (num - num2)) : 0f); - bounds = base.Bounds; - float num4 = ((SKRect)(ref bounds)).Right - ScrollBarWidth; - bounds = base.Bounds; - float num5 = ((SKRect)(ref bounds)).Top + num3; - bounds = base.Bounds; - float right = ((SKRect)(ref bounds)).Right; - bounds = base.Bounds; - return new SKRect(num4, num5, right, ((SKRect)(ref bounds)).Top + num3 + num2); - } + canvas.DrawRoundRect(thumbRect, paint); + } - private SKRect GetHorizontalScrollbarThumbBounds() - { - //IL_0008: Unknown result type (might be due to invalid IL or missing references) - //IL_000d: Unknown result type (might be due to invalid IL or missing references) - //IL_002e: Unknown result type (might be due to invalid IL or missing references) - //IL_0033: Unknown result type (might be due to invalid IL or missing references) - //IL_003d: Unknown result type (might be due to invalid IL or missing references) - //IL_0042: Unknown result type (might be due to invalid IL or missing references) - //IL_007b: Unknown result type (might be due to invalid IL or missing references) - //IL_0080: Unknown result type (might be due to invalid IL or missing references) - //IL_008c: Unknown result type (might be due to invalid IL or missing references) - //IL_0091: Unknown result type (might be due to invalid IL or missing references) - //IL_00a2: Unknown result type (might be due to invalid IL or missing references) - //IL_00a7: Unknown result type (might be due to invalid IL or missing references) - //IL_00b5: Unknown result type (might be due to invalid IL or missing references) - //IL_00ba: Unknown result type (might be due to invalid IL or missing references) - //IL_00c3: Unknown result type (might be due to invalid IL or missing references) - bool flag = ShouldShowVerticalScrollbar(); - SKRect bounds = base.Bounds; - float num = ((SKRect)(ref bounds)).Width - (flag ? ScrollBarWidth : 0f); - bounds = base.Bounds; - float width = ((SKRect)(ref bounds)).Width; - SKSize contentSize = ContentSize; - float num2 = Math.Max(20f, width / ((SKSize)(ref contentSize)).Width * num); - float num3 = ((ScrollableWidth > 0f) ? (ScrollX / ScrollableWidth * (num - num2)) : 0f); - bounds = base.Bounds; - float num4 = ((SKRect)(ref bounds)).Left + num3; - bounds = base.Bounds; - float num5 = ((SKRect)(ref bounds)).Bottom - ScrollBarWidth; - bounds = base.Bounds; - float num6 = ((SKRect)(ref bounds)).Left + num3 + num2; - bounds = base.Bounds; - return new SKRect(num4, num5, num6, ((SKRect)(ref bounds)).Bottom); - } + private void DrawHorizontalScrollbar(SKCanvas canvas, SKRect bounds, bool hasVertical) + { + var trackWidth = bounds.Width - (hasVertical ? ScrollBarWidth : 0); + var thumbWidth = Math.Max(20, (bounds.Width / ContentSize.Width) * trackWidth); + var thumbX = (ScrollX / ScrollableWidth) * (trackWidth - thumbWidth); - public override SkiaView? HitTest(float x, float y) - { - //IL_0011: Unknown result type (might be due to invalid IL or missing references) - //IL_0016: Unknown result type (might be due to invalid IL or missing references) - //IL_001b: Unknown result type (might be due to invalid IL or missing references) - //IL_003f: Unknown result type (might be due to invalid IL or missing references) - //IL_0048: Unknown result type (might be due to invalid IL or missing references) - //IL_004d: Unknown result type (might be due to invalid IL or missing references) - //IL_005d: Unknown result type (might be due to invalid IL or missing references) - //IL_0062: Unknown result type (might be due to invalid IL or missing references) - //IL_006b: Unknown result type (might be due to invalid IL or missing references) - //IL_0070: Unknown result type (might be due to invalid IL or missing references) - //IL_0079: Unknown result type (might be due to invalid IL or missing references) - //IL_007e: Unknown result type (might be due to invalid IL or missing references) - //IL_00b0: Unknown result type (might be due to invalid IL or missing references) - //IL_00b5: Unknown result type (might be due to invalid IL or missing references) - //IL_00be: Unknown result type (might be due to invalid IL or missing references) - //IL_00c3: Unknown result type (might be due to invalid IL or missing references) - //IL_00d3: Unknown result type (might be due to invalid IL or missing references) - //IL_00d8: Unknown result type (might be due to invalid IL or missing references) - //IL_00e1: Unknown result type (might be due to invalid IL or missing references) - //IL_00e6: Unknown result type (might be due to invalid IL or missing references) - if (base.IsVisible && base.IsEnabled) - { - SKRect bounds = base.Bounds; - if (((SKRect)(ref bounds)).Contains(new SKPoint(x, y))) - { - if (ShouldShowVerticalScrollbar() && ScrollableHeight > 0f) - { - GetVerticalScrollbarThumbBounds(); - bounds = base.Bounds; - float num = ((SKRect)(ref bounds)).Right - ScrollBarWidth; - bounds = base.Bounds; - float top = ((SKRect)(ref bounds)).Top; - bounds = base.Bounds; - float right = ((SKRect)(ref bounds)).Right; - bounds = base.Bounds; - SKRect val = default(SKRect); - ((SKRect)(ref val))._002Ector(num, top, right, ((SKRect)(ref bounds)).Bottom); - if (((SKRect)(ref val)).Contains(x, y)) - { - return this; - } - } - if (ShouldShowHorizontalScrollbar() && ScrollableWidth > 0f) - { - bounds = base.Bounds; - float left = ((SKRect)(ref bounds)).Left; - bounds = base.Bounds; - float num2 = ((SKRect)(ref bounds)).Bottom - ScrollBarWidth; - bounds = base.Bounds; - float right2 = ((SKRect)(ref bounds)).Right; - bounds = base.Bounds; - SKRect val2 = default(SKRect); - ((SKRect)(ref val2))._002Ector(left, num2, right2, ((SKRect)(ref bounds)).Bottom); - if (((SKRect)(ref val2)).Contains(x, y)) - { - return this; - } - } - if (_content != null) - { - SkiaView skiaView = _content.HitTest(x + _scrollX, y + _scrollY); - if (skiaView != null) - { - return skiaView; - } - } - return this; - } - } - return null; - } + using var paint = new SKPaint + { + Color = ScrollBarColor, + IsAntialias = true + }; - public void ScrollTo(float x, float y, bool animated = false) - { - ScrollX = x; - ScrollY = y; - } + var thumbRect = new SKRoundRect( + new SKRect( + bounds.Left + thumbX, + bounds.Bottom - ScrollBarWidth, + bounds.Left + thumbX + thumbWidth, + bounds.Bottom), + ScrollBarWidth / 2); - public void ScrollToView(SkiaView view, bool animated = false) - { - //IL_000a: Unknown result type (might be due to invalid IL or missing references) - //IL_000f: Unknown result type (might be due to invalid IL or missing references) - //IL_0025: Unknown result type (might be due to invalid IL or missing references) - //IL_002a: Unknown result type (might be due to invalid IL or missing references) - //IL_003b: Unknown result type (might be due to invalid IL or missing references) - //IL_0040: Unknown result type (might be due to invalid IL or missing references) - //IL_0051: Unknown result type (might be due to invalid IL or missing references) - //IL_009a: Unknown result type (might be due to invalid IL or missing references) - //IL_009f: Unknown result type (might be due to invalid IL or missing references) - //IL_00dc: Unknown result type (might be due to invalid IL or missing references) - //IL_00e1: Unknown result type (might be due to invalid IL or missing references) - if (_content == null) - { - return; - } - SKRect bounds = view.Bounds; - float scrollX = ScrollX; - float scrollY = ScrollY; - float scrollX2 = ScrollX; - SKRect bounds2 = base.Bounds; - float num = scrollX2 + ((SKRect)(ref bounds2)).Width; - float scrollY2 = ScrollY; - bounds2 = base.Bounds; - SKRect val = default(SKRect); - ((SKRect)(ref val))._002Ector(scrollX, scrollY, num, scrollY2 + ((SKRect)(ref bounds2)).Height); - if (!((SKRect)(ref val)).Contains(bounds)) - { - float x = ScrollX; - float y = ScrollY; - if (((SKRect)(ref bounds)).Left < ((SKRect)(ref val)).Left) - { - x = ((SKRect)(ref bounds)).Left; - } - else if (((SKRect)(ref bounds)).Right > ((SKRect)(ref val)).Right) - { - float right = ((SKRect)(ref bounds)).Right; - bounds2 = base.Bounds; - x = right - ((SKRect)(ref bounds2)).Width; - } - if (((SKRect)(ref bounds)).Top < ((SKRect)(ref val)).Top) - { - y = ((SKRect)(ref bounds)).Top; - } - else if (((SKRect)(ref bounds)).Bottom > ((SKRect)(ref val)).Bottom) - { - float bottom = ((SKRect)(ref bounds)).Bottom; - bounds2 = base.Bounds; - y = bottom - ((SKRect)(ref bounds2)).Height; - } - ScrollTo(x, y, animated); - } - } + canvas.DrawRoundRect(thumbRect, paint); + } - private float ClampScrollX(float value) - { - if (Orientation == ScrollOrientation.Vertical) - { - return 0f; - } - return Math.Clamp(value, 0f, ScrollableWidth); - } + public override void OnScroll(ScrollEventArgs e) + { + Console.WriteLine($"[SkiaScrollView] OnScroll - DeltaY={e.DeltaY}, ScrollableHeight={ScrollableHeight}, ContentSize={ContentSize}, Bounds={Bounds}"); - private float ClampScrollY(float value) - { - if (Orientation == ScrollOrientation.Horizontal) - { - return 0f; - } - return Math.Clamp(value, 0f, ScrollableHeight); - } + // Handle mouse wheel scrolling + var deltaMultiplier = 40f; // Scroll speed + bool scrolled = false; - protected override SKSize MeasureOverride(SKSize availableSize) - { - //IL_0118: Unknown result type (might be due to invalid IL or missing references) - //IL_0148: Unknown result type (might be due to invalid IL or missing references) - //IL_014d: Unknown result type (might be due to invalid IL or missing references) - //IL_0186: Unknown result type (might be due to invalid IL or missing references) - //IL_018b: Unknown result type (might be due to invalid IL or missing references) - //IL_0106: Unknown result type (might be due to invalid IL or missing references) - //IL_010b: Unknown result type (might be due to invalid IL or missing references) - //IL_01a0: Unknown result type (might be due to invalid IL or missing references) - if (_content != null) - { - float num; - float num2; - switch (Orientation) - { - case ScrollOrientation.Horizontal: - num = float.PositiveInfinity; - num2 = (float.IsInfinity(((SKSize)(ref availableSize)).Height) ? 400f : ((SKSize)(ref availableSize)).Height); - break; - case ScrollOrientation.Neither: - num = (float.IsInfinity(((SKSize)(ref availableSize)).Width) ? 400f : ((SKSize)(ref availableSize)).Width); - num2 = (float.IsInfinity(((SKSize)(ref availableSize)).Height) ? 400f : ((SKSize)(ref availableSize)).Height); - break; - case ScrollOrientation.Both: - num = (float.IsInfinity(((SKSize)(ref availableSize)).Width) ? 800f : ((SKSize)(ref availableSize)).Width); - if (VerticalScrollBarVisibility != ScrollBarVisibility.Never) - { - num -= ScrollBarWidth; - } - num2 = float.PositiveInfinity; - break; - default: - num = (float.IsInfinity(((SKSize)(ref availableSize)).Width) ? 800f : ((SKSize)(ref availableSize)).Width); - if (VerticalScrollBarVisibility != ScrollBarVisibility.Never) - { - num -= ScrollBarWidth; - } - num2 = float.PositiveInfinity; - break; - } - ContentSize = _content.Measure(new SKSize(num, num2)); - } - else - { - ContentSize = SKSize.Empty; - } - float num3; - SKSize contentSize; - if (!float.IsInfinity(((SKSize)(ref availableSize)).Width) && !float.IsNaN(((SKSize)(ref availableSize)).Width)) - { - num3 = ((SKSize)(ref availableSize)).Width; - } - else - { - contentSize = ContentSize; - num3 = Math.Min(((SKSize)(ref contentSize)).Width, 400f); - } - float num4; - if (!float.IsInfinity(((SKSize)(ref availableSize)).Height) && !float.IsNaN(((SKSize)(ref availableSize)).Height)) - { - num4 = ((SKSize)(ref availableSize)).Height; - } - else - { - contentSize = ContentSize; - num4 = Math.Min(((SKSize)(ref contentSize)).Height, 400f); - } - float num5 = num4; - return new SKSize(num3, num5); - } + if (Orientation != ScrollOrientation.Horizontal && ScrollableHeight > 0) + { + var oldScrollY = _scrollY; + ScrollY += e.DeltaY * deltaMultiplier; + Console.WriteLine($"[SkiaScrollView] ScrollY changed: {oldScrollY} -> {_scrollY}"); + if (_scrollY != oldScrollY) + scrolled = true; + } - protected override SKRect ArrangeOverride(SKRect bounds) - { - //IL_0000: Unknown result type (might be due to invalid IL or missing references) - //IL_0001: Unknown result type (might be due to invalid IL or missing references) - //IL_0117: Unknown result type (might be due to invalid IL or missing references) - //IL_0086: Unknown result type (might be due to invalid IL or missing references) - //IL_008b: Unknown result type (might be due to invalid IL or missing references) - //IL_00bd: Unknown result type (might be due to invalid IL or missing references) - //IL_00c2: Unknown result type (might be due to invalid IL or missing references) - //IL_00e9: Unknown result type (might be due to invalid IL or missing references) - //IL_00ee: Unknown result type (might be due to invalid IL or missing references) - //IL_0111: Unknown result type (might be due to invalid IL or missing references) - SKRect result = bounds; - if (float.IsInfinity(((SKRect)(ref bounds)).Height) || float.IsNaN(((SKRect)(ref bounds)).Height)) - { - Console.WriteLine($"[SkiaScrollView] WARNING: Infinite/NaN height, using default viewport={544f}"); - ((SKRect)(ref result))._002Ector(((SKRect)(ref bounds)).Left, ((SKRect)(ref bounds)).Top, ((SKRect)(ref bounds)).Right, ((SKRect)(ref bounds)).Top + 544f); - } - if (_content != null) - { - Thickness margin = _content.Margin; - float num = ((SKRect)(ref result)).Left + (float)((Thickness)(ref margin)).Left; - float num2 = ((SKRect)(ref result)).Top + (float)((Thickness)(ref margin)).Top; - float left = ((SKRect)(ref result)).Left; - float width = ((SKRect)(ref result)).Width; - SKSize contentSize = ContentSize; - float num3 = left + Math.Max(width, ((SKSize)(ref contentSize)).Width) - (float)((Thickness)(ref margin)).Right; - float top = ((SKRect)(ref result)).Top; - float height = ((SKRect)(ref result)).Height; - contentSize = ContentSize; - SKRect bounds2 = default(SKRect); - ((SKRect)(ref bounds2))._002Ector(num, num2, num3, top + Math.Max(height, ((SKSize)(ref contentSize)).Height) - (float)((Thickness)(ref margin)).Bottom); - _content.Arrange(bounds2); - } - return result; - } + if (Orientation != ScrollOrientation.Vertical && ScrollableWidth > 0) + { + var oldScrollX = _scrollX; + ScrollX += e.DeltaX * deltaMultiplier; + if (_scrollX != oldScrollX) + scrolled = true; + } + + // Mark as handled so parent scroll views don't also scroll + if (scrolled) + e.Handled = true; + } + + public override void OnPointerPressed(PointerEventArgs e) + { + // Check if clicking on vertical scrollbar thumb + if (ShouldShowVerticalScrollbar() && ScrollableHeight > 0) + { + var thumbBounds = GetVerticalScrollbarThumbBounds(); + if (thumbBounds.Contains(e.X, e.Y)) + { + _isDraggingVerticalScrollbar = true; + _scrollbarDragStartY = e.Y; + _scrollbarDragStartScrollY = _scrollY; + // Cache values to prevent stutter from floating-point recalculations + var hasHorizontal = ShouldShowHorizontalScrollbar(); + var trackHeight = Bounds.Height - (hasHorizontal ? ScrollBarWidth : 0); + var thumbHeight = Math.Max(20, (Bounds.Height / ContentSize.Height) * trackHeight); + _scrollbarDragAvailableTrack = trackHeight - thumbHeight; + _scrollbarDragScrollableExtent = ScrollableHeight; + return; + } + } + + // Check if clicking on horizontal scrollbar thumb + if (ShouldShowHorizontalScrollbar() && ScrollableWidth > 0) + { + var thumbBounds = GetHorizontalScrollbarThumbBounds(); + if (thumbBounds.Contains(e.X, e.Y)) + { + _isDraggingHorizontalScrollbar = true; + _scrollbarDragStartX = e.X; + _scrollbarDragStartScrollX = _scrollX; + // Cache values to prevent stutter from floating-point recalculations + var hasVertical = ShouldShowVerticalScrollbar(); + var trackWidth = Bounds.Width - (hasVertical ? ScrollBarWidth : 0); + var thumbWidth = Math.Max(20, (Bounds.Width / ContentSize.Width) * trackWidth); + _scrollbarDragAvailableTrack = trackWidth - thumbWidth; + _scrollbarDragScrollableExtent = ScrollableWidth; + return; + } + } + + // Forward click to content first + if (_content != null) + { + // Translate coordinates for scroll offset + var contentE = new PointerEventArgs(e.X + _scrollX, e.Y + _scrollY, e.Button); + var hit = _content.HitTest(contentE.X, contentE.Y); + if (hit != null && hit != _content) + { + // A child view was hit - forward the event to it + hit.OnPointerPressed(contentE); + return; + } + } + + // Regular content dragging + _isDragging = true; + _lastPointerX = e.X; + _lastPointerY = e.Y; + _velocityX = 0; + _velocityY = 0; + } + + public override void OnPointerMoved(PointerEventArgs e) + { + // Handle vertical scrollbar dragging - use cached values to prevent stutter + if (_isDraggingVerticalScrollbar) + { + if (_scrollbarDragAvailableTrack > 0) + { + var deltaY = e.Y - _scrollbarDragStartY; + var scrollDelta = (deltaY / _scrollbarDragAvailableTrack) * _scrollbarDragScrollableExtent; + ScrollY = _scrollbarDragStartScrollY + scrollDelta; + } + return; + } + + // Handle horizontal scrollbar dragging - use cached values to prevent stutter + if (_isDraggingHorizontalScrollbar) + { + if (_scrollbarDragAvailableTrack > 0) + { + var deltaX = e.X - _scrollbarDragStartX; + var scrollDelta = (deltaX / _scrollbarDragAvailableTrack) * _scrollbarDragScrollableExtent; + ScrollX = _scrollbarDragStartScrollX + scrollDelta; + } + return; + } + + // Handle content dragging + if (!_isDragging) return; + + var contentDeltaX = _lastPointerX - e.X; + var contentDeltaY = _lastPointerY - e.Y; + + _velocityX = contentDeltaX; + _velocityY = contentDeltaY; + + if (Orientation != ScrollOrientation.Horizontal) + ScrollY += contentDeltaY; + + if (Orientation != ScrollOrientation.Vertical) + ScrollX += contentDeltaX; + + _lastPointerX = e.X; + _lastPointerY = e.Y; + } + + public override void OnPointerReleased(PointerEventArgs e) + { + _isDragging = false; + _isDraggingVerticalScrollbar = false; + _isDraggingHorizontalScrollbar = false; + // Momentum scrolling could be added here + } + + private SKRect GetVerticalScrollbarThumbBounds() + { + var hasHorizontal = ShouldShowHorizontalScrollbar(); + var trackHeight = Bounds.Height - (hasHorizontal ? ScrollBarWidth : 0); + var thumbHeight = Math.Max(20, (Bounds.Height / ContentSize.Height) * trackHeight); + var thumbY = ScrollableHeight > 0 ? (ScrollY / ScrollableHeight) * (trackHeight - thumbHeight) : 0; + + return new SKRect( + Bounds.Right - ScrollBarWidth, + Bounds.Top + thumbY, + Bounds.Right, + Bounds.Top + thumbY + thumbHeight); + } + + private SKRect GetHorizontalScrollbarThumbBounds() + { + var hasVertical = ShouldShowVerticalScrollbar(); + var trackWidth = Bounds.Width - (hasVertical ? ScrollBarWidth : 0); + var thumbWidth = Math.Max(20, (Bounds.Width / ContentSize.Width) * trackWidth); + var thumbX = ScrollableWidth > 0 ? (ScrollX / ScrollableWidth) * (trackWidth - thumbWidth) : 0; + + return new SKRect( + Bounds.Left + thumbX, + Bounds.Bottom - ScrollBarWidth, + Bounds.Left + thumbX + thumbWidth, + Bounds.Bottom); + } + + public override SkiaView? HitTest(float x, float y) + { + if (!IsVisible || !IsEnabled || !Bounds.Contains(new SKPoint(x, y))) + return null; + + // Check scrollbar areas FIRST before content + // This ensures scrollbar clicks are handled by the ScrollView, not content underneath + if (ShouldShowVerticalScrollbar() && ScrollableHeight > 0) + { + var thumbBounds = GetVerticalScrollbarThumbBounds(); + // Check if click is in the scrollbar track area (not just thumb) + var trackArea = new SKRect(Bounds.Right - ScrollBarWidth, Bounds.Top, Bounds.Right, Bounds.Bottom); + if (trackArea.Contains(x, y)) + return this; + } + + if (ShouldShowHorizontalScrollbar() && ScrollableWidth > 0) + { + var trackArea = new SKRect(Bounds.Left, Bounds.Bottom - ScrollBarWidth, Bounds.Right, Bounds.Bottom); + if (trackArea.Contains(x, y)) + return this; + } + + // Hit test content with scroll offset + if (_content != null) + { + var hit = _content.HitTest(x + _scrollX, y + _scrollY); + if (hit != null) + return hit; + } + + return this; + } + + /// + /// Scrolls to the specified position. + /// + public void ScrollTo(float x, float y, bool animated = false) + { + // TODO: Implement animation + ScrollX = x; + ScrollY = y; + } + + /// + /// Scrolls to make the specified view visible. + /// + public void ScrollToView(SkiaView view, bool animated = false) + { + if (_content == null) return; + + var viewBounds = view.Bounds; + + // Check if view is fully visible + var visibleRect = new SKRect( + ScrollX, + ScrollY, + ScrollX + Bounds.Width, + ScrollY + Bounds.Height); + + if (visibleRect.Contains(viewBounds)) + return; + + // Calculate scroll position to bring view into view + float targetX = ScrollX; + float targetY = ScrollY; + + if (viewBounds.Left < visibleRect.Left) + targetX = viewBounds.Left; + else if (viewBounds.Right > visibleRect.Right) + targetX = viewBounds.Right - Bounds.Width; + + if (viewBounds.Top < visibleRect.Top) + targetY = viewBounds.Top; + else if (viewBounds.Bottom > visibleRect.Bottom) + targetY = viewBounds.Bottom - Bounds.Height; + + ScrollTo(targetX, targetY, animated); + } + + private float ClampScrollX(float value) + { + if (Orientation == ScrollOrientation.Vertical) return 0; + return Math.Clamp(value, 0, ScrollableWidth); + } + + private float ClampScrollY(float value) + { + if (Orientation == ScrollOrientation.Horizontal) return 0; + return Math.Clamp(value, 0, ScrollableHeight); + } + + protected override SKSize MeasureOverride(SKSize availableSize) + { + if (_content != null) + { + // For responsive layout: + // - Vertical: give content viewport width, infinite height + // - Horizontal: give content infinite width, viewport height + // - Both: give content viewport width first (for responsive layout), + // but if content exceeds it, horizontal scrollbar appears + // - Neither: give content exact viewport size + + float contentWidth, contentHeight; + + switch (Orientation) + { + case ScrollOrientation.Horizontal: + contentWidth = float.PositiveInfinity; + contentHeight = float.IsInfinity(availableSize.Height) ? 400f : availableSize.Height; + break; + case ScrollOrientation.Neither: + contentWidth = float.IsInfinity(availableSize.Width) ? 400f : availableSize.Width; + contentHeight = float.IsInfinity(availableSize.Height) ? 400f : availableSize.Height; + break; + case ScrollOrientation.Both: + // For Both: first measure with viewport width to get responsive layout + // Content can still exceed viewport if it has minimum width constraints + // Reserve space for vertical scrollbar to prevent horizontal scrollbar + contentWidth = float.IsInfinity(availableSize.Width) ? 800f : availableSize.Width; + if (VerticalScrollBarVisibility != ScrollBarVisibility.Never) + contentWidth -= ScrollBarWidth; + contentHeight = float.PositiveInfinity; + break; + case ScrollOrientation.Vertical: + default: + // Reserve space for vertical scrollbar to prevent horizontal scrollbar + contentWidth = float.IsInfinity(availableSize.Width) ? 800f : availableSize.Width; + if (VerticalScrollBarVisibility != ScrollBarVisibility.Never) + contentWidth -= ScrollBarWidth; + contentHeight = float.PositiveInfinity; + break; + } + + ContentSize = _content.Measure(new SKSize(contentWidth, contentHeight)); + } + else + { + ContentSize = SKSize.Empty; + } + + // Return available size, but clamp infinite dimensions + // IMPORTANT: When available is infinite, return a reasonable viewport size, NOT content size + // A ScrollView should NOT expand to fit its content - it should stay at a fixed viewport + // and scroll the content. Use a default viewport size when parent gives infinity. + const float DefaultViewportWidth = 400f; + const float DefaultViewportHeight = 400f; + + var width = float.IsInfinity(availableSize.Width) || float.IsNaN(availableSize.Width) + ? Math.Min(ContentSize.Width, DefaultViewportWidth) + : availableSize.Width; + var height = float.IsInfinity(availableSize.Height) || float.IsNaN(availableSize.Height) + ? Math.Min(ContentSize.Height, DefaultViewportHeight) + : availableSize.Height; + + return new SKSize(width, height); + } + + protected override SKRect ArrangeOverride(SKRect bounds) + { + + // CRITICAL: If bounds has infinite height, use a fixed viewport size + // NOT ContentSize.Height - that would make ScrollableHeight = 0 + const float DefaultViewportHeight = 544f; // 600 - 56 for shell header + var actualBounds = bounds; + if (float.IsInfinity(bounds.Height) || float.IsNaN(bounds.Height)) + { + Console.WriteLine($"[SkiaScrollView] WARNING: Infinite/NaN height, using default viewport={DefaultViewportHeight}"); + actualBounds = new SKRect(bounds.Left, bounds.Top, bounds.Right, bounds.Top + DefaultViewportHeight); + } + + if (_content != null) + { + // Apply content's margin and arrange content at its full size + var margin = _content.Margin; + var contentBounds = new SKRect( + actualBounds.Left + (float)margin.Left, + actualBounds.Top + (float)margin.Top, + actualBounds.Left + Math.Max(actualBounds.Width, ContentSize.Width) - (float)margin.Right, + actualBounds.Top + Math.Max(actualBounds.Height, ContentSize.Height) - (float)margin.Bottom); + + _content.Arrange(contentBounds); + } + return actualBounds; + } +} + +/// +/// Scroll orientation options. +/// +public enum ScrollOrientation +{ + Vertical, + Horizontal, + Both, + Neither +} + +/// +/// Scrollbar visibility options. +/// +public enum ScrollBarVisibility +{ + Default, + Always, + Never, + Auto +} + +/// +/// Event args for scroll events. +/// +public class ScrolledEventArgs : EventArgs +{ + public float ScrollX { get; } + public float ScrollY { get; } + + public ScrolledEventArgs(float scrollX, float scrollY) + { + ScrollX = scrollX; + ScrollY = scrollY; + } } diff --git a/Views/SkiaSearchBar.cs b/Views/SkiaSearchBar.cs index c5af480..64824c9 100644 --- a/Views/SkiaSearchBar.cs +++ b/Views/SkiaSearchBar.cs @@ -1,356 +1,242 @@ -using System; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using SkiaSharp; +using Microsoft.Maui.Platform.Linux.Rendering; namespace Microsoft.Maui.Platform; +/// +/// Skia-rendered search bar control. +/// public class SkiaSearchBar : SkiaView { - private readonly SkiaEntry _entry; + private readonly SkiaEntry _entry; + private bool _showClearButton; - private bool _showClearButton; + public string Text + { + get => _entry.Text; + set => _entry.Text = value; + } - public string Text - { - get - { - return _entry.Text; - } - set - { - _entry.Text = value; - } - } + public string Placeholder + { + get => _entry.Placeholder; + set => _entry.Placeholder = value; + } - public string Placeholder - { - get - { - return _entry.Placeholder; - } - set - { - _entry.Placeholder = value; - } - } + public SKColor TextColor + { + get => _entry.TextColor; + set => _entry.TextColor = value; + } - public SKColor TextColor - { - get - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - return _entry.TextColor; - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - _entry.TextColor = value; - } - } + public SKColor PlaceholderColor + { + get => _entry.PlaceholderColor; + set => _entry.PlaceholderColor = value; + } - public SKColor PlaceholderColor - { - get - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - return _entry.PlaceholderColor; - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - _entry.PlaceholderColor = value; - } - } + public new SKColor BackgroundColor { get; set; } = new SKColor(0xF5, 0xF5, 0xF5); + public SKColor IconColor { get; set; } = new SKColor(0x75, 0x75, 0x75); + public SKColor ClearButtonColor { get; set; } = new SKColor(0x9E, 0x9E, 0x9E); + public SKColor FocusedBorderColor { get; set; } = new SKColor(0x21, 0x96, 0xF3); + public string FontFamily { get; set; } = "Sans"; + public float FontSize { get; set; } = 14; + public float CornerRadius { get; set; } = 8; + public float IconSize { get; set; } = 20; - public new SKColor BackgroundColor { get; set; } = new SKColor((byte)245, (byte)245, (byte)245); + public event EventHandler? TextChanged; + public event EventHandler? SearchButtonPressed; - public SKColor IconColor { get; set; } = new SKColor((byte)117, (byte)117, (byte)117); + public SkiaSearchBar() + { + _entry = new SkiaEntry + { + Placeholder = "Search...", + EntryBackgroundColor = SKColors.Transparent, + BackgroundColor = SKColors.Transparent, + BorderColor = SKColors.Transparent, + FocusedBorderColor = SKColors.Transparent, + BorderWidth = 0 + }; - public SKColor ClearButtonColor { get; set; } = new SKColor((byte)158, (byte)158, (byte)158); + _entry.TextChanged += (s, e) => + { + _showClearButton = !string.IsNullOrEmpty(e.NewTextValue); + TextChanged?.Invoke(this, e); + Invalidate(); + }; - public SKColor FocusedBorderColor { get; set; } = new SKColor((byte)33, (byte)150, (byte)243); + _entry.Completed += (s, e) => SearchButtonPressed?.Invoke(this, EventArgs.Empty); - public string FontFamily { get; set; } = "Sans"; + IsFocusable = true; + } - public float FontSize { get; set; } = 14f; + protected override void OnDraw(SKCanvas canvas, SKRect bounds) + { + var iconPadding = 12f; + var clearButtonSize = 20f; - public float CornerRadius { get; set; } = 8f; + // Draw background + using var bgPaint = new SKPaint + { + Color = BackgroundColor, + IsAntialias = true, + Style = SKPaintStyle.Fill + }; - public float IconSize { get; set; } = 20f; + var bgRect = new SKRoundRect(bounds, CornerRadius); + canvas.DrawRoundRect(bgRect, bgPaint); - public event EventHandler? TextChanged; + // Draw focus border + if (IsFocused || _entry.IsFocused) + { + using var borderPaint = new SKPaint + { + Color = FocusedBorderColor, + IsAntialias = true, + Style = SKPaintStyle.Stroke, + StrokeWidth = 2 + }; + canvas.DrawRoundRect(bgRect, borderPaint); + } - public event EventHandler? SearchButtonPressed; + // Draw search icon + var iconX = bounds.Left + iconPadding; + var iconY = bounds.MidY; + DrawSearchIcon(canvas, iconX, iconY, IconSize); - public SkiaSearchBar() - { - //IL_0010: Unknown result type (might be due to invalid IL or missing references) - //IL_0015: Unknown result type (might be due to invalid IL or missing references) - //IL_0021: Unknown result type (might be due to invalid IL or missing references) - //IL_0026: Unknown result type (might be due to invalid IL or missing references) - //IL_003b: Unknown result type (might be due to invalid IL or missing references) - //IL_0040: Unknown result type (might be due to invalid IL or missing references) - //IL_0052: Unknown result type (might be due to invalid IL or missing references) - //IL_0057: Unknown result type (might be due to invalid IL or missing references) - //IL_00a0: Unknown result type (might be due to invalid IL or missing references) - //IL_00ab: Unknown result type (might be due to invalid IL or missing references) - //IL_00b6: Unknown result type (might be due to invalid IL or missing references) - //IL_00c1: Unknown result type (might be due to invalid IL or missing references) - _entry = new SkiaEntry - { - Placeholder = "Search...", - EntryBackgroundColor = SKColors.Transparent, - BackgroundColor = SKColors.Transparent, - BorderColor = SKColors.Transparent, - FocusedBorderColor = SKColors.Transparent, - BorderWidth = 0f - }; - _entry.TextChanged += delegate(object? s, TextChangedEventArgs e) - { - _showClearButton = !string.IsNullOrEmpty(e.NewTextValue); - this.TextChanged?.Invoke(this, e); - Invalidate(); - }; - _entry.Completed += delegate - { - this.SearchButtonPressed?.Invoke(this, EventArgs.Empty); - }; - base.IsFocusable = true; - } + // Calculate entry bounds - leave space for clear button + var entryLeft = iconX + IconSize + iconPadding; + var entryRight = _showClearButton + ? bounds.Right - clearButtonSize - iconPadding * 2 + : bounds.Right - iconPadding; - protected override void OnDraw(SKCanvas canvas, SKRect bounds) - { - //IL_000c: Unknown result type (might be due to invalid IL or missing references) - //IL_0011: Unknown result type (might be due to invalid IL or missing references) - //IL_0013: Unknown result type (might be due to invalid IL or missing references) - //IL_001d: Unknown result type (might be due to invalid IL or missing references) - //IL_0024: Unknown result type (might be due to invalid IL or missing references) - //IL_002c: Expected O, but got Unknown - //IL_002c: Unknown result type (might be due to invalid IL or missing references) - //IL_0033: Unknown result type (might be due to invalid IL or missing references) - //IL_0039: Expected O, but got Unknown - //IL_0056: Unknown result type (might be due to invalid IL or missing references) - //IL_005b: Unknown result type (might be due to invalid IL or missing references) - //IL_005d: Unknown result type (might be due to invalid IL or missing references) - //IL_0067: Unknown result type (might be due to invalid IL or missing references) - //IL_006e: Unknown result type (might be due to invalid IL or missing references) - //IL_0075: Unknown result type (might be due to invalid IL or missing references) - //IL_0082: Expected O, but got Unknown - //IL_0103: Unknown result type (might be due to invalid IL or missing references) - //IL_0108: Unknown result type (might be due to invalid IL or missing references) - //IL_0110: Unknown result type (might be due to invalid IL or missing references) - float num = 12f; - float num2 = 20f; - SKPaint val = new SKPaint - { - Color = BackgroundColor, - IsAntialias = true, - Style = (SKPaintStyle)0 - }; - try - { - SKRoundRect val2 = new SKRoundRect(bounds, CornerRadius); - canvas.DrawRoundRect(val2, val); - if (base.IsFocused || _entry.IsFocused) - { - SKPaint val3 = new SKPaint - { - Color = FocusedBorderColor, - IsAntialias = true, - Style = (SKPaintStyle)1, - StrokeWidth = 2f - }; - try - { - canvas.DrawRoundRect(val2, val3); - } - finally - { - ((IDisposable)val3)?.Dispose(); - } - } - float num3 = ((SKRect)(ref bounds)).Left + num; - float midY = ((SKRect)(ref bounds)).MidY; - DrawSearchIcon(canvas, num3, midY, IconSize); - float num4 = num3 + IconSize + num; - float num5 = (_showClearButton ? (((SKRect)(ref bounds)).Right - num2 - num * 2f) : (((SKRect)(ref bounds)).Right - num)); - SKRect bounds2 = new SKRect(num4, ((SKRect)(ref bounds)).Top, num5, ((SKRect)(ref bounds)).Bottom); - _entry.Arrange(bounds2); - _entry.Draw(canvas); - if (_showClearButton) - { - float x = ((SKRect)(ref bounds)).Right - num - num2 / 2f; - float midY2 = ((SKRect)(ref bounds)).MidY; - DrawClearButton(canvas, x, midY2, num2 / 2f); - } - } - finally - { - ((IDisposable)val)?.Dispose(); - } - } + var entryBounds = new SKRect(entryLeft, bounds.Top, entryRight, bounds.Bottom); + _entry.Arrange(entryBounds); + _entry.Draw(canvas); - private void DrawSearchIcon(SKCanvas canvas, float x, float y, float size) - { - //IL_0000: Unknown result type (might be due to invalid IL or missing references) - //IL_0005: Unknown result type (might be due to invalid IL or missing references) - //IL_0007: Unknown result type (might be due to invalid IL or missing references) - //IL_0011: Unknown result type (might be due to invalid IL or missing references) - //IL_0018: Unknown result type (might be due to invalid IL or missing references) - //IL_001f: Unknown result type (might be due to invalid IL or missing references) - //IL_002a: Unknown result type (might be due to invalid IL or missing references) - //IL_0032: Expected O, but got Unknown - //IL_0047: Unknown result type (might be due to invalid IL or missing references) - //IL_004c: Unknown result type (might be due to invalid IL or missing references) - //IL_004e: Unknown result type (might be due to invalid IL or missing references) - //IL_0074: Unknown result type (might be due to invalid IL or missing references) - //IL_0079: Unknown result type (might be due to invalid IL or missing references) - //IL_008e: Unknown result type (might be due to invalid IL or missing references) - //IL_0093: Unknown result type (might be due to invalid IL or missing references) - //IL_0096: Unknown result type (might be due to invalid IL or missing references) - //IL_0097: Unknown result type (might be due to invalid IL or missing references) - SKPaint val = new SKPaint - { - Color = IconColor, - IsAntialias = true, - Style = (SKPaintStyle)1, - StrokeWidth = 2f, - StrokeCap = (SKStrokeCap)1 - }; - try - { - float num = size * 0.35f; - SKPoint val2 = new SKPoint(x + num, y - num * 0.3f); - canvas.DrawCircle(val2, num, val); - SKPoint val3 = new SKPoint(((SKPoint)(ref val2)).X + num * 0.7f, ((SKPoint)(ref val2)).Y + num * 0.7f); - SKPoint val4 = new SKPoint(x + size * 0.8f, y + size * 0.3f); - canvas.DrawLine(val3, val4, val); - } - finally - { - ((IDisposable)val)?.Dispose(); - } - } + // Draw clear button + if (_showClearButton) + { + var clearX = bounds.Right - iconPadding - clearButtonSize / 2; + var clearY = bounds.MidY; + DrawClearButton(canvas, clearX, clearY, clearButtonSize / 2); + } + } - private void DrawClearButton(SKCanvas canvas, float x, float y, float radius) - { - //IL_0000: Unknown result type (might be due to invalid IL or missing references) - //IL_0005: Unknown result type (might be due to invalid IL or missing references) - //IL_0007: Unknown result type (might be due to invalid IL or missing references) - //IL_000c: Unknown result type (might be due to invalid IL or missing references) - //IL_0011: Unknown result type (might be due to invalid IL or missing references) - //IL_001b: Unknown result type (might be due to invalid IL or missing references) - //IL_0022: Unknown result type (might be due to invalid IL or missing references) - //IL_002a: Expected O, but got Unknown - //IL_003b: Unknown result type (might be due to invalid IL or missing references) - //IL_0040: Unknown result type (might be due to invalid IL or missing references) - //IL_0042: Unknown result type (might be due to invalid IL or missing references) - //IL_004c: Unknown result type (might be due to invalid IL or missing references) - //IL_0053: Unknown result type (might be due to invalid IL or missing references) - //IL_005a: Unknown result type (might be due to invalid IL or missing references) - //IL_0065: Unknown result type (might be due to invalid IL or missing references) - //IL_006d: Expected O, but got Unknown - SKPaint val = new SKPaint(); - SKColor clearButtonColor = ClearButtonColor; - val.Color = ((SKColor)(ref clearButtonColor)).WithAlpha((byte)80); - val.IsAntialias = true; - val.Style = (SKPaintStyle)0; - SKPaint val2 = val; - try - { - canvas.DrawCircle(x, y, radius + 2f, val2); - SKPaint val3 = new SKPaint - { - Color = ClearButtonColor, - IsAntialias = true, - Style = (SKPaintStyle)1, - StrokeWidth = 2f, - StrokeCap = (SKStrokeCap)1 - }; - try - { - float num = radius * 0.5f; - canvas.DrawLine(x - num, y - num, x + num, y + num, val3); - canvas.DrawLine(x + num, y - num, x - num, y + num, val3); - } - finally - { - ((IDisposable)val3)?.Dispose(); - } - } - finally - { - ((IDisposable)val2)?.Dispose(); - } - } + private void DrawSearchIcon(SKCanvas canvas, float x, float y, float size) + { + using var paint = new SKPaint + { + Color = IconColor, + IsAntialias = true, + Style = SKPaintStyle.Stroke, + StrokeWidth = 2, + StrokeCap = SKStrokeCap.Round + }; - public override void OnPointerPressed(PointerEventArgs e) - { - //IL_0010: Unknown result type (might be due to invalid IL or missing references) - //IL_0015: Unknown result type (might be due to invalid IL or missing references) - //IL_0029: Unknown result type (might be due to invalid IL or missing references) - //IL_002e: Unknown result type (might be due to invalid IL or missing references) - if (!base.IsEnabled) - { - return; - } - float x = e.X; - SKRect bounds = base.Bounds; - float num = x - ((SKRect)(ref bounds)).Left; - if (_showClearButton) - { - bounds = base.Bounds; - if (num >= ((SKRect)(ref bounds)).Width - 40f) - { - Text = ""; - Invalidate(); - return; - } - } - _entry.IsFocused = true; - base.IsFocused = true; - _entry.OnPointerPressed(e); - Invalidate(); - } + var circleRadius = size * 0.35f; + var circleCenter = new SKPoint(x + circleRadius, y - circleRadius * 0.3f); - public override void OnPointerMoved(PointerEventArgs e) - { - if (base.IsEnabled) - { - _entry.OnPointerMoved(e); - } - } + // Draw magnifying glass circle + canvas.DrawCircle(circleCenter, circleRadius, paint); - public override void OnPointerReleased(PointerEventArgs e) - { - _entry.OnPointerReleased(e); - } + // Draw handle + var handleStart = new SKPoint( + circleCenter.X + circleRadius * 0.7f, + circleCenter.Y + circleRadius * 0.7f); + var handleEnd = new SKPoint( + x + size * 0.8f, + y + size * 0.3f); + canvas.DrawLine(handleStart, handleEnd, paint); + } - public override void OnTextInput(TextInputEventArgs e) - { - _entry.OnTextInput(e); - } + private void DrawClearButton(SKCanvas canvas, float x, float y, float radius) + { + // Draw circle background + using var bgPaint = new SKPaint + { + Color = ClearButtonColor.WithAlpha(80), + IsAntialias = true, + Style = SKPaintStyle.Fill + }; + canvas.DrawCircle(x, y, radius + 2, bgPaint); - public override void OnKeyDown(KeyEventArgs e) - { - if (e.Key == Key.Escape && _showClearButton) - { - Text = ""; - e.Handled = true; - } - else - { - _entry.OnKeyDown(e); - } - } + // Draw X + using var paint = new SKPaint + { + Color = ClearButtonColor, + IsAntialias = true, + Style = SKPaintStyle.Stroke, + StrokeWidth = 2, + StrokeCap = SKStrokeCap.Round + }; - public override void OnKeyUp(KeyEventArgs e) - { - _entry.OnKeyUp(e); - } + var offset = radius * 0.5f; + canvas.DrawLine(x - offset, y - offset, x + offset, y + offset, paint); + canvas.DrawLine(x + offset, y - offset, x - offset, y + offset, paint); + } - protected override SKSize MeasureOverride(SKSize availableSize) - { - //IL_000a: Unknown result type (might be due to invalid IL or missing references) - return new SKSize(250f, 40f); - } + public override void OnPointerPressed(PointerEventArgs e) + { + if (!IsEnabled) return; + + // Convert to local coordinates (relative to this view's bounds) + var localX = e.X - Bounds.Left; + + // Check if clear button was clicked (in the rightmost 40 pixels) + if (_showClearButton && localX >= Bounds.Width - 40) + { + Text = ""; + Invalidate(); + return; + } + + // Forward to entry for text input focus and selection + _entry.IsFocused = true; + IsFocused = true; + _entry.OnPointerPressed(e); + Invalidate(); + } + + public override void OnPointerMoved(PointerEventArgs e) + { + if (!IsEnabled) return; + _entry.OnPointerMoved(e); + } + + public override void OnPointerReleased(PointerEventArgs e) + { + _entry.OnPointerReleased(e); + } + + public override void OnTextInput(TextInputEventArgs e) + { + _entry.OnTextInput(e); + } + + public override void OnKeyDown(KeyEventArgs e) + { + if (e.Key == Key.Escape && _showClearButton) + { + Text = ""; + e.Handled = true; + return; + } + + _entry.OnKeyDown(e); + } + + public override void OnKeyUp(KeyEventArgs e) + { + _entry.OnKeyUp(e); + } + + protected override SKSize MeasureOverride(SKSize availableSize) + { + return new SKSize(250, 40); + } } diff --git a/Views/SkiaSelectionMode.cs b/Views/SkiaSelectionMode.cs deleted file mode 100644 index 5b5841e..0000000 --- a/Views/SkiaSelectionMode.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Microsoft.Maui.Platform; - -public enum SkiaSelectionMode -{ - None, - Single, - Multiple -} diff --git a/Views/SkiaShell.cs b/Views/SkiaShell.cs index 79a8855..c131c8f 100644 --- a/Views/SkiaShell.cs +++ b/Views/SkiaShell.cs @@ -1,1325 +1,918 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using Microsoft.Maui.Controls; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using SkiaSharp; namespace Microsoft.Maui.Platform; +/// +/// Shell provides a common navigation experience for MAUI applications. +/// Supports flyout menu, tabs, and URI-based navigation. +/// public class SkiaShell : SkiaLayoutView { - public static readonly BindableProperty FlyoutIsPresentedProperty = BindableProperty.Create("FlyoutIsPresented", typeof(bool), typeof(SkiaShell), (object)false, (BindingMode)1, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaShell)(object)b).OnFlyoutIsPresentedChanged((bool)n); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty FlyoutBehaviorProperty = BindableProperty.Create("FlyoutBehavior", typeof(ShellFlyoutBehavior), typeof(SkiaShell), (object)ShellFlyoutBehavior.Flyout, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaShell)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty FlyoutWidthProperty = BindableProperty.Create("FlyoutWidth", typeof(float), typeof(SkiaShell), (object)280f, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaShell)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)((BindableObject b, object v) => Math.Max(100f, (float)v)), (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty FlyoutBackgroundColorProperty = BindableProperty.Create("FlyoutBackgroundColor", typeof(SKColor), typeof(SkiaShell), (object)SKColors.White, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaShell)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty FlyoutTextColorProperty = BindableProperty.Create("FlyoutTextColor", typeof(SKColor), typeof(SkiaShell), (object)new SKColor((byte)33, (byte)33, (byte)33), (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaShell)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty NavBarBackgroundColorProperty = BindableProperty.Create("NavBarBackgroundColor", typeof(SKColor), typeof(SkiaShell), (object)new SKColor((byte)33, (byte)150, (byte)243), (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaShell)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty NavBarTextColorProperty = BindableProperty.Create("NavBarTextColor", typeof(SKColor), typeof(SkiaShell), (object)SKColors.White, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaShell)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty NavBarHeightProperty = BindableProperty.Create("NavBarHeight", typeof(float), typeof(SkiaShell), (object)56f, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaShell)(object)b).InvalidateMeasure(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty TabBarHeightProperty = BindableProperty.Create("TabBarHeight", typeof(float), typeof(SkiaShell), (object)56f, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaShell)(object)b).InvalidateMeasure(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty NavBarIsVisibleProperty = BindableProperty.Create("NavBarIsVisible", typeof(bool), typeof(SkiaShell), (object)true, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaShell)(object)b).InvalidateMeasure(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty TabBarIsVisibleProperty = BindableProperty.Create("TabBarIsVisible", typeof(bool), typeof(SkiaShell), (object)false, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaShell)(object)b).InvalidateMeasure(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty ContentPaddingProperty = BindableProperty.Create("ContentPadding", typeof(float), typeof(SkiaShell), (object)0f, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaShell)(object)b).InvalidateMeasure(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty ContentBackgroundColorProperty = BindableProperty.Create("ContentBackgroundColor", typeof(SKColor), typeof(SkiaShell), (object)new SKColor((byte)250, (byte)250, (byte)250), (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaShell)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty TitleProperty = BindableProperty.Create("Title", typeof(string), typeof(SkiaShell), (object)string.Empty, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaShell)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - private readonly List _sections = new List(); - - private SkiaView? _currentContent; - - private float _flyoutAnimationProgress; - - private int _selectedSectionIndex; - - private int _selectedItemIndex; - - private readonly Stack<(SkiaView Content, string Title)> _navigationStack = new Stack<(SkiaView, string)>(); - - private float _flyoutScrollOffset; - - private readonly Dictionary> _registeredRoutes = new Dictionary>(StringComparer.OrdinalIgnoreCase); - - private readonly Dictionary _routeTitles = new Dictionary(StringComparer.OrdinalIgnoreCase); - - public bool FlyoutIsPresented - { - get - { - return (bool)((BindableObject)this).GetValue(FlyoutIsPresentedProperty); - } - set - { - ((BindableObject)this).SetValue(FlyoutIsPresentedProperty, (object)value); - } - } - - public ShellFlyoutBehavior FlyoutBehavior - { - get - { - return (ShellFlyoutBehavior)((BindableObject)this).GetValue(FlyoutBehaviorProperty); - } - set - { - ((BindableObject)this).SetValue(FlyoutBehaviorProperty, (object)value); - } - } - - public float FlyoutWidth - { - get - { - return (float)((BindableObject)this).GetValue(FlyoutWidthProperty); - } - set - { - ((BindableObject)this).SetValue(FlyoutWidthProperty, (object)value); - } - } - - public SKColor FlyoutBackgroundColor - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKColor)((BindableObject)this).GetValue(FlyoutBackgroundColorProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(FlyoutBackgroundColorProperty, (object)value); - } - } - - public SKColor FlyoutTextColor - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKColor)((BindableObject)this).GetValue(FlyoutTextColorProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(FlyoutTextColorProperty, (object)value); - } - } - - public SkiaView? FlyoutHeaderView { get; set; } - - public float FlyoutHeaderHeight { get; set; } = 140f; - - public string? FlyoutFooterText { get; set; } - - public float FlyoutFooterHeight { get; set; } = 40f; - - public SKColor NavBarBackgroundColor - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKColor)((BindableObject)this).GetValue(NavBarBackgroundColorProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(NavBarBackgroundColorProperty, (object)value); - } - } - - public SKColor NavBarTextColor - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKColor)((BindableObject)this).GetValue(NavBarTextColorProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(NavBarTextColorProperty, (object)value); - } - } - - public float NavBarHeight - { - get - { - return (float)((BindableObject)this).GetValue(NavBarHeightProperty); - } - set - { - ((BindableObject)this).SetValue(NavBarHeightProperty, (object)value); - } - } - - public float TabBarHeight - { - get - { - return (float)((BindableObject)this).GetValue(TabBarHeightProperty); - } - set - { - ((BindableObject)this).SetValue(TabBarHeightProperty, (object)value); - } - } - - public bool NavBarIsVisible - { - get - { - return (bool)((BindableObject)this).GetValue(NavBarIsVisibleProperty); - } - set - { - ((BindableObject)this).SetValue(NavBarIsVisibleProperty, (object)value); - } - } - - public bool TabBarIsVisible - { - get - { - return (bool)((BindableObject)this).GetValue(TabBarIsVisibleProperty); - } - set - { - ((BindableObject)this).SetValue(TabBarIsVisibleProperty, (object)value); - } - } - - public float ContentPadding - { - get - { - return (float)((BindableObject)this).GetValue(ContentPaddingProperty); - } - set - { - ((BindableObject)this).SetValue(ContentPaddingProperty, (object)value); - } - } - - public SKColor ContentBackgroundColor - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKColor)((BindableObject)this).GetValue(ContentBackgroundColorProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(ContentBackgroundColorProperty, (object)value); - } - } - - public string Title - { - get - { - return (string)((BindableObject)this).GetValue(TitleProperty); - } - set - { - ((BindableObject)this).SetValue(TitleProperty, (object)value); - } - } - - public IReadOnlyList Sections => _sections; - - public int CurrentSectionIndex => _selectedSectionIndex; - - public Func? ContentRenderer { get; set; } - - public Action? ColorRefresher { get; set; } - - public Shell? MauiShell { get; set; } - - public bool CanGoBack => _navigationStack.Count > 0; - - public int NavigationStackDepth => _navigationStack.Count; - - public event EventHandler? FlyoutIsPresentedChanged; - - public event EventHandler? Navigated; - - private void OnFlyoutIsPresentedChanged(bool newValue) - { - _flyoutAnimationProgress = (newValue ? 1f : 0f); - this.FlyoutIsPresentedChanged?.Invoke(this, EventArgs.Empty); - Invalidate(); - } - - public void RefreshTheme() - { - Console.WriteLine("[SkiaShell] RefreshTheme called - refreshing all pages"); - if (MauiShell != null && ColorRefresher != null) - { - Console.WriteLine("[SkiaShell] Refreshing shell colors"); - ColorRefresher(this, MauiShell); - } - if (ContentRenderer != null) - { - foreach (ShellSection section in _sections) - { - foreach (ShellContent item in section.Items) - { - if (item.MauiShellContent != null) - { - Console.WriteLine("[SkiaShell] Re-rendering: " + item.Title); - SkiaView skiaView = ContentRenderer(item.MauiShellContent); - if (skiaView != null) - { - item.Content = skiaView; - } - } - } - } - } - if (_selectedSectionIndex >= 0 && _selectedSectionIndex < _sections.Count) - { - ShellSection shellSection = _sections[_selectedSectionIndex]; - if (_selectedItemIndex >= 0 && _selectedItemIndex < shellSection.Items.Count) - { - ShellContent shellContent = shellSection.Items[_selectedItemIndex]; - SetCurrentContent(shellContent.Content); - } - } - InvalidateMeasure(); - Invalidate(); - } - - public void AddSection(ShellSection section) - { - _sections.Add(section); - if (_sections.Count == 1) - { - NavigateToSection(0); - } - Invalidate(); - } - - public void RemoveSection(ShellSection section) - { - _sections.Remove(section); - Invalidate(); - } - - public void NavigateToSection(int sectionIndex, int itemIndex = 0) - { - if (sectionIndex >= 0 && sectionIndex < _sections.Count) - { - ShellSection shellSection = _sections[sectionIndex]; - if (itemIndex >= 0 && itemIndex < shellSection.Items.Count) - { - _navigationStack.Clear(); - _selectedSectionIndex = sectionIndex; - _selectedItemIndex = itemIndex; - ShellContent shellContent = shellSection.Items[itemIndex]; - SetCurrentContent(shellContent.Content); - Title = shellContent.Title; - this.Navigated?.Invoke(this, new ShellNavigationEventArgs(shellSection, shellContent)); - Invalidate(); - } - } - } - - public void GoToAsync(string route) - { - GoToAsync(route, null); - } - - public void GoToAsync(string route, IDictionary? parameters) - { - if (string.IsNullOrEmpty(route)) - { - return; - } - string text = route; - Dictionary dictionary = new Dictionary(); - int num = route.IndexOf('?'); - if (num >= 0) - { - text = route.Substring(0, num); - dictionary = ParseQueryString(route.Substring(num + 1)); - } - Dictionary dictionary2 = new Dictionary(); - foreach (KeyValuePair item in dictionary) - { - dictionary2[item.Key] = item.Value; - } - if (parameters != null) - { - foreach (KeyValuePair parameter in parameters) - { - dictionary2[parameter.Key] = parameter.Value; - } - } - string[] array = text.TrimStart('/').Split('/'); - if (array.Length == 0) - { - return; - } - if (_registeredRoutes.TryGetValue(text.TrimStart('/'), out Func value)) - { - SkiaView skiaView = value(); - if (skiaView != null) - { - ApplyQueryParameters(skiaView, dictionary2); - PushAsync(skiaView, GetRouteTitle(text.TrimStart('/'))); - return; - } - } - for (int i = 0; i < _sections.Count; i++) - { - ShellSection shellSection = _sections[i]; - if (!shellSection.Route.Equals(array[0], StringComparison.OrdinalIgnoreCase)) - { - continue; - } - if (array.Length > 1) - { - for (int j = 0; j < shellSection.Items.Count; j++) - { - if (shellSection.Items[j].Route.Equals(array[1], StringComparison.OrdinalIgnoreCase)) - { - NavigateToSection(i, j); - if (shellSection.Items[j].Content != null && dictionary2.Count > 0) - { - ApplyQueryParameters(shellSection.Items[j].Content, dictionary2); - } - return; - } - } - } - NavigateToSection(i); - if (shellSection.Items.Count > 0 && shellSection.Items[0].Content != null && dictionary2.Count > 0) - { - ApplyQueryParameters(shellSection.Items[0].Content, dictionary2); - } - break; - } - } - - private static Dictionary ParseQueryString(string queryString) - { - Dictionary dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); - if (string.IsNullOrEmpty(queryString)) - { - return dictionary; - } - string[] array = queryString.Split('&', StringSplitOptions.RemoveEmptyEntries); - for (int i = 0; i < array.Length; i++) - { - string[] array2 = array[i].Split('=', 2); - if (array2.Length == 2) - { - string key = Uri.UnescapeDataString(array2[0]); - string value = Uri.UnescapeDataString(array2[1]); - dictionary[key] = value; - } - else if (array2.Length == 1) - { - dictionary[Uri.UnescapeDataString(array2[0])] = string.Empty; - } - } - return dictionary; - } - - private static void ApplyQueryParameters(SkiaView content, IDictionary parameters) - { - if (parameters.Count == 0) - { - return; - } - if (content is ISkiaQueryAttributable skiaQueryAttributable) - { - skiaQueryAttributable.ApplyQueryAttributes(parameters); - } - Type type = ((object)content).GetType(); - foreach (KeyValuePair parameter in parameters) - { - PropertyInfo property = type.GetProperty(parameter.Key, BindingFlags.IgnoreCase | BindingFlags.Instance | BindingFlags.Public); - if (property != null && property.CanWrite) - { - try - { - object value = Convert.ChangeType(parameter.Value, property.PropertyType); - property.SetValue(content, value); - } - catch - { - } - } - } - } - - public void RegisterRoute(string route, Func contentFactory, string? title = null) - { - string key = route.TrimStart('/'); - _registeredRoutes[key] = contentFactory; - if (!string.IsNullOrEmpty(title)) - { - _routeTitles[key] = title; - } - } - - public void UnregisterRoute(string route) - { - string key = route.TrimStart('/'); - _registeredRoutes.Remove(key); - _routeTitles.Remove(key); - } - - private string GetRouteTitle(string route) - { - if (_routeTitles.TryGetValue(route, out string value)) - { - return value; - } - return route.Split('/').LastOrDefault() ?? route; - } - - public void PushAsync(SkiaView page, string title) - { - if (_currentContent != null) - { - _navigationStack.Push((_currentContent, Title)); - } - SetCurrentContent(page); - Title = title; - Invalidate(); - } - - public bool PopAsync() - { - if (_navigationStack.Count == 0) - { - return false; - } - var (currentContent, title) = _navigationStack.Pop(); - SetCurrentContent(currentContent); - Title = title; - Invalidate(); - return true; - } - - public void PopToRootAsync() - { - if (_navigationStack.Count != 0) - { - (SkiaView, string) tuple = default((SkiaView, string)); - while (_navigationStack.Count > 0) - { - tuple = _navigationStack.Pop(); - } - SetCurrentContent(tuple.Item1); - Title = tuple.Item2; - Invalidate(); - } - } - - private void SetCurrentContent(SkiaView? content) - { - if (_currentContent != null) - { - RemoveChild(_currentContent); - } - _currentContent = content; - if (_currentContent != null) - { - AddChild(_currentContent); - } - } - - protected override SKSize MeasureOverride(SKSize availableSize) - { - //IL_009d: Unknown result type (might be due to invalid IL or missing references) - //IL_0041: Unknown result type (might be due to invalid IL or missing references) - //IL_0046: Unknown result type (might be due to invalid IL or missing references) - //IL_0051: Unknown result type (might be due to invalid IL or missing references) - //IL_0056: Unknown result type (might be due to invalid IL or missing references) - //IL_006c: Unknown result type (might be due to invalid IL or missing references) - //IL_0071: Unknown result type (might be due to invalid IL or missing references) - //IL_007c: Unknown result type (might be due to invalid IL or missing references) - //IL_0081: Unknown result type (might be due to invalid IL or missing references) - //IL_0096: Unknown result type (might be due to invalid IL or missing references) - //IL_0097: Unknown result type (might be due to invalid IL or missing references) - if (_currentContent != null) - { - float num = (NavBarIsVisible ? NavBarHeight : 0f); - float num2 = (TabBarIsVisible ? TabBarHeight : 0f); - float width = ((SKSize)(ref availableSize)).Width; - SKRect padding = base.Padding; - float num3 = width - ((SKRect)(ref padding)).Left; - padding = base.Padding; - float num4 = num3 - ((SKRect)(ref padding)).Right; - float num5 = ((SKSize)(ref availableSize)).Height - num - num2; - padding = base.Padding; - float num6 = num5 - ((SKRect)(ref padding)).Top; - padding = base.Padding; - SKSize availableSize2 = default(SKSize); - ((SKSize)(ref availableSize2))._002Ector(num4, num6 - ((SKRect)(ref padding)).Bottom); - _currentContent.Measure(availableSize2); - } - return availableSize; - } - - protected override SKRect ArrangeOverride(SKRect bounds) - { - //IL_0018: Unknown result type (might be due to invalid IL or missing references) - //IL_00f3: Unknown result type (might be due to invalid IL or missing references) - //IL_00bc: Unknown result type (might be due to invalid IL or missing references) - //IL_00ed: Unknown result type (might be due to invalid IL or missing references) - Console.WriteLine($"[SkiaShell] ArrangeOverride - bounds={bounds}"); - if (_currentContent != null) - { - float num = ((SKRect)(ref bounds)).Top + (NavBarIsVisible ? NavBarHeight : 0f) + ContentPadding; - float num2 = ((SKRect)(ref bounds)).Bottom - (TabBarIsVisible ? TabBarHeight : 0f) - ContentPadding; - SKRect val = default(SKRect); - ((SKRect)(ref val))._002Ector(((SKRect)(ref bounds)).Left + ContentPadding, num, ((SKRect)(ref bounds)).Right - ContentPadding, num2); - Console.WriteLine($"[SkiaShell] Arranging content with bounds={val}, padding={ContentPadding}"); - _currentContent.Arrange(val); - } - return bounds; - } - - protected override void OnDraw(SKCanvas canvas, SKRect bounds) - { - //IL_0008: Unknown result type (might be due to invalid IL or missing references) - //IL_0063: Unknown result type (might be due to invalid IL or missing references) - //IL_0068: Unknown result type (might be due to invalid IL or missing references) - //IL_006a: Unknown result type (might be due to invalid IL or missing references) - //IL_0074: Unknown result type (might be due to invalid IL or missing references) - //IL_007c: Expected O, but got Unknown - //IL_007d: Unknown result type (might be due to invalid IL or missing references) - //IL_00a0: Unknown result type (might be due to invalid IL or missing references) - //IL_00b0: Unknown result type (might be due to invalid IL or missing references) - //IL_00c5: Unknown result type (might be due to invalid IL or missing references) - canvas.Save(); - canvas.ClipRect(bounds, (SKClipOperation)1, false); - float num = ((SKRect)(ref bounds)).Top + (NavBarIsVisible ? NavBarHeight : 0f); - float num2 = ((SKRect)(ref bounds)).Bottom - (TabBarIsVisible ? TabBarHeight : 0f); - SKRect val = default(SKRect); - ((SKRect)(ref val))._002Ector(((SKRect)(ref bounds)).Left, num, ((SKRect)(ref bounds)).Right, num2); - SKPaint val2 = new SKPaint - { - Color = ContentBackgroundColor, - Style = (SKPaintStyle)0 - }; - try - { - canvas.DrawRect(val, val2); - _currentContent?.Draw(canvas); - if (NavBarIsVisible) - { - DrawNavBar(canvas, bounds); - } - if (TabBarIsVisible) - { - DrawTabBar(canvas, bounds); - } - if (_flyoutAnimationProgress > 0f) - { - DrawFlyout(canvas, bounds); - } - canvas.Restore(); - } - finally - { - ((IDisposable)val2)?.Dispose(); - } - } - - private void DrawNavBar(SKCanvas canvas, SKRect bounds) - { - //IL_002a: Unknown result type (might be due to invalid IL or missing references) - //IL_002f: Unknown result type (might be due to invalid IL or missing references) - //IL_0031: Unknown result type (might be due to invalid IL or missing references) - //IL_003b: Unknown result type (might be due to invalid IL or missing references) - //IL_0042: Unknown result type (might be due to invalid IL or missing references) - //IL_004a: Expected O, but got Unknown - //IL_004b: Unknown result type (might be due to invalid IL or missing references) - //IL_0052: Unknown result type (might be due to invalid IL or missing references) - //IL_0057: Unknown result type (might be due to invalid IL or missing references) - //IL_0059: Unknown result type (might be due to invalid IL or missing references) - //IL_0063: Unknown result type (might be due to invalid IL or missing references) - //IL_006a: Unknown result type (might be due to invalid IL or missing references) - //IL_0075: Unknown result type (might be due to invalid IL or missing references) - //IL_007c: Unknown result type (might be due to invalid IL or missing references) - //IL_0084: Expected O, but got Unknown - //IL_00a6: Unknown result type (might be due to invalid IL or missing references) - //IL_00ab: Unknown result type (might be due to invalid IL or missing references) - //IL_00ad: Unknown result type (might be due to invalid IL or missing references) - //IL_00b7: Unknown result type (might be due to invalid IL or missing references) - //IL_00be: Unknown result type (might be due to invalid IL or missing references) - //IL_00c9: Unknown result type (might be due to invalid IL or missing references) - //IL_00d0: Unknown result type (might be due to invalid IL or missing references) - //IL_00d7: Unknown result type (might be due to invalid IL or missing references) - //IL_00e0: Expected O, but got Unknown - //IL_0184: Unknown result type (might be due to invalid IL or missing references) - //IL_0189: Unknown result type (might be due to invalid IL or missing references) - //IL_018b: Unknown result type (might be due to invalid IL or missing references) - //IL_0195: Unknown result type (might be due to invalid IL or missing references) - //IL_01a0: Unknown result type (might be due to invalid IL or missing references) - //IL_01a7: Unknown result type (might be due to invalid IL or missing references) - //IL_01b0: Expected O, but got Unknown - SKRect val = default(SKRect); - ((SKRect)(ref val))._002Ector(((SKRect)(ref bounds)).Left, ((SKRect)(ref bounds)).Top, ((SKRect)(ref bounds)).Right, ((SKRect)(ref bounds)).Top + NavBarHeight); - SKPaint val2 = new SKPaint - { - Color = NavBarBackgroundColor, - Style = (SKPaintStyle)0, - IsAntialias = true - }; - try - { - canvas.DrawRect(val, val2); - SKPaint val3 = new SKPaint - { - Color = NavBarTextColor, - Style = (SKPaintStyle)1, - StrokeWidth = 2f, - StrokeCap = (SKStrokeCap)1, - IsAntialias = true - }; - try - { - float num = ((SKRect)(ref val)).Left + 16f; - float midY = ((SKRect)(ref val)).MidY; - if (CanGoBack) - { - SKPaint val4 = new SKPaint - { - Color = NavBarTextColor, - Style = (SKPaintStyle)1, - StrokeWidth = 2.5f, - StrokeCap = (SKStrokeCap)1, - StrokeJoin = (SKStrokeJoin)1, - IsAntialias = true - }; - try - { - float num2 = num + 6f; - float num3 = 10f; - canvas.DrawLine(num2 + num3, midY - num3, num2, midY, val4); - canvas.DrawLine(num2, midY, num2 + num3, midY + num3, val4); - } - finally - { - ((IDisposable)val4)?.Dispose(); - } - } - else if (FlyoutBehavior == ShellFlyoutBehavior.Flyout) - { - canvas.DrawLine(num, midY - 8f, num + 18f, midY - 8f, val3); - canvas.DrawLine(num, midY, num + 18f, midY, val3); - canvas.DrawLine(num, midY + 8f, num + 18f, midY + 8f, val3); - } - SKPaint val5 = new SKPaint - { - Color = NavBarTextColor, - TextSize = 20f, - IsAntialias = true, - FakeBoldText = true - }; - try - { - float num4 = ((CanGoBack || FlyoutBehavior == ShellFlyoutBehavior.Flyout) ? (((SKRect)(ref val)).Left + 56f) : (((SKRect)(ref val)).Left + 16f)); - float num5 = ((SKRect)(ref val)).MidY + 6f; - canvas.DrawText(Title, num4, num5, val5); - } - finally - { - ((IDisposable)val5)?.Dispose(); - } - } - finally - { - ((IDisposable)val3)?.Dispose(); - } - } - finally - { - ((IDisposable)val2)?.Dispose(); - } - } - - private void DrawTabBar(SKCanvas canvas, SKRect bounds) - { - //IL_0068: Unknown result type (might be due to invalid IL or missing references) - //IL_006d: Unknown result type (might be due to invalid IL or missing references) - //IL_006e: Unknown result type (might be due to invalid IL or missing references) - //IL_0078: Unknown result type (might be due to invalid IL or missing references) - //IL_007f: Unknown result type (might be due to invalid IL or missing references) - //IL_0087: Expected O, but got Unknown - //IL_0088: Unknown result type (might be due to invalid IL or missing references) - //IL_008f: Unknown result type (might be due to invalid IL or missing references) - //IL_0094: Unknown result type (might be due to invalid IL or missing references) - //IL_00a4: Unknown result type (might be due to invalid IL or missing references) - //IL_00ae: Unknown result type (might be due to invalid IL or missing references) - //IL_00b5: Unknown result type (might be due to invalid IL or missing references) - //IL_00c1: Expected O, but got Unknown - //IL_00fa: Unknown result type (might be due to invalid IL or missing references) - //IL_00ff: Unknown result type (might be due to invalid IL or missing references) - //IL_010a: Unknown result type (might be due to invalid IL or missing references) - //IL_0113: Expected O, but got Unknown - //IL_014a: Unknown result type (might be due to invalid IL or missing references) - //IL_0142: Unknown result type (might be due to invalid IL or missing references) - //IL_0156: Unknown result type (might be due to invalid IL or missing references) - if (_selectedSectionIndex < 0 || _selectedSectionIndex >= _sections.Count) - { - return; - } - ShellSection shellSection = _sections[_selectedSectionIndex]; - if (shellSection.Items.Count <= 1) - { - return; - } - SKRect val = default(SKRect); - ((SKRect)(ref val))._002Ector(((SKRect)(ref bounds)).Left, ((SKRect)(ref bounds)).Bottom - TabBarHeight, ((SKRect)(ref bounds)).Right, ((SKRect)(ref bounds)).Bottom); - SKPaint val2 = new SKPaint - { - Color = SKColors.White, - Style = (SKPaintStyle)0, - IsAntialias = true - }; - try - { - canvas.DrawRect(val, val2); - SKPaint val3 = new SKPaint - { - Color = new SKColor((byte)224, (byte)224, (byte)224), - Style = (SKPaintStyle)1, - StrokeWidth = 1f - }; - try - { - canvas.DrawLine(((SKRect)(ref val)).Left, ((SKRect)(ref val)).Top, ((SKRect)(ref val)).Right, ((SKRect)(ref val)).Top, val3); - float num = ((SKRect)(ref val)).Width / (float)shellSection.Items.Count; - SKPaint val4 = new SKPaint - { - TextSize = 12f, - IsAntialias = true - }; - try - { - for (int i = 0; i < shellSection.Items.Count; i++) - { - ShellContent shellContent = shellSection.Items[i]; - bool flag = i == _selectedItemIndex; - val4.Color = (SKColor)(flag ? NavBarBackgroundColor : new SKColor((byte)117, (byte)117, (byte)117)); - SKRect val5 = default(SKRect); - val4.MeasureText(shellContent.Title, ref val5); - float num2 = ((SKRect)(ref val)).Left + (float)i * num + num / 2f - ((SKRect)(ref val5)).MidX; - float num3 = ((SKRect)(ref val)).MidY - ((SKRect)(ref val5)).MidY; - canvas.DrawText(shellContent.Title, num2, num3, val4); - } - } - finally - { - ((IDisposable)val4)?.Dispose(); - } - } - finally - { - ((IDisposable)val3)?.Dispose(); - } - } - finally - { - ((IDisposable)val2)?.Dispose(); - } - } - - private void DrawFlyout(SKCanvas canvas, SKRect bounds) - { - //IL_0000: Unknown result type (might be due to invalid IL or missing references) - //IL_0005: Unknown result type (might be due to invalid IL or missing references) - //IL_0016: Unknown result type (might be due to invalid IL or missing references) - //IL_0020: Unknown result type (might be due to invalid IL or missing references) - //IL_0028: Expected O, but got Unknown - //IL_0029: Unknown result type (might be due to invalid IL or missing references) - //IL_0064: Unknown result type (might be due to invalid IL or missing references) - //IL_0069: Unknown result type (might be due to invalid IL or missing references) - //IL_006a: Unknown result type (might be due to invalid IL or missing references) - //IL_006f: Unknown result type (might be due to invalid IL or missing references) - //IL_0071: Unknown result type (might be due to invalid IL or missing references) - //IL_007b: Unknown result type (might be due to invalid IL or missing references) - //IL_0082: Unknown result type (might be due to invalid IL or missing references) - //IL_008a: Expected O, but got Unknown - //IL_008b: Unknown result type (might be due to invalid IL or missing references) - //IL_01ef: Unknown result type (might be due to invalid IL or missing references) - //IL_01fb: Unknown result type (might be due to invalid IL or missing references) - //IL_0200: Unknown result type (might be due to invalid IL or missing references) - //IL_020b: Unknown result type (might be due to invalid IL or missing references) - //IL_0214: Expected O, but got Unknown - //IL_014c: Unknown result type (might be due to invalid IL or missing references) - //IL_0193: Unknown result type (might be due to invalid IL or missing references) - //IL_0198: Unknown result type (might be due to invalid IL or missing references) - //IL_01a4: Unknown result type (might be due to invalid IL or missing references) - //IL_033d: Unknown result type (might be due to invalid IL or missing references) - //IL_0342: Unknown result type (might be due to invalid IL or missing references) - //IL_0344: Unknown result type (might be due to invalid IL or missing references) - //IL_0349: Unknown result type (might be due to invalid IL or missing references) - //IL_034f: Unknown result type (might be due to invalid IL or missing references) - //IL_0359: Unknown result type (might be due to invalid IL or missing references) - //IL_0360: Unknown result type (might be due to invalid IL or missing references) - //IL_036d: Expected O, but got Unknown - //IL_0432: Unknown result type (might be due to invalid IL or missing references) - //IL_0437: Unknown result type (might be due to invalid IL or missing references) - //IL_0439: Unknown result type (might be due to invalid IL or missing references) - //IL_043e: Unknown result type (might be due to invalid IL or missing references) - //IL_0444: Unknown result type (might be due to invalid IL or missing references) - //IL_044e: Unknown result type (might be due to invalid IL or missing references) - //IL_0455: Unknown result type (might be due to invalid IL or missing references) - //IL_045e: Expected O, but got Unknown - //IL_0393: Unknown result type (might be due to invalid IL or missing references) - //IL_0398: Unknown result type (might be due to invalid IL or missing references) - //IL_03a3: Unknown result type (might be due to invalid IL or missing references) - //IL_03a5: Unknown result type (might be due to invalid IL or missing references) - //IL_03aa: Unknown result type (might be due to invalid IL or missing references) - //IL_03b3: Unknown result type (might be due to invalid IL or missing references) - //IL_03bd: Unknown result type (might be due to invalid IL or missing references) - //IL_03c6: Expected O, but got Unknown - //IL_0264: Unknown result type (might be due to invalid IL or missing references) - //IL_0269: Unknown result type (might be due to invalid IL or missing references) - //IL_0278: Unknown result type (might be due to invalid IL or missing references) - //IL_0282: Unknown result type (might be due to invalid IL or missing references) - //IL_028b: Expected O, but got Unknown - //IL_049d: Unknown result type (might be due to invalid IL or missing references) - //IL_04a7: Unknown result type (might be due to invalid IL or missing references) - //IL_04b3: Expected O, but got Unknown - //IL_03c8: Unknown result type (might be due to invalid IL or missing references) - //IL_02ce: Unknown result type (might be due to invalid IL or missing references) - //IL_02c6: Unknown result type (might be due to invalid IL or missing references) - //IL_02a0: Unknown result type (might be due to invalid IL or missing references) - //IL_02a5: Unknown result type (might be due to invalid IL or missing references) - //IL_02a8: Unknown result type (might be due to invalid IL or missing references) - SKPaint val = new SKPaint - { - Color = new SKColor((byte)0, (byte)0, (byte)0, (byte)(100f * _flyoutAnimationProgress)), - Style = (SKPaintStyle)0 - }; - try - { - canvas.DrawRect(bounds, val); - float num = ((SKRect)(ref bounds)).Left - FlyoutWidth + FlyoutWidth * _flyoutAnimationProgress; - SKRect val2 = new SKRect(num, ((SKRect)(ref bounds)).Top, num + FlyoutWidth, ((SKRect)(ref bounds)).Bottom); - SKPaint val3 = new SKPaint - { - Color = FlyoutBackgroundColor, - Style = (SKPaintStyle)0, - IsAntialias = true - }; - try - { - canvas.DrawRect(val2, val3); - float num2 = ((FlyoutHeaderView != null) ? FlyoutHeaderHeight : 0f); - float num3 = ((!string.IsNullOrEmpty(FlyoutFooterText)) ? FlyoutFooterHeight : 0f); - float num4 = 48f; - float num5 = (float)_sections.Count * num4; - float num6 = ((SKRect)(ref val2)).Height - num2 - num3; - float num7 = Math.Max(0f, num5 - num6); - _flyoutScrollOffset = Math.Max(0f, Math.Min(_flyoutScrollOffset, num7)); - if (FlyoutHeaderView != null) - { - canvas.Save(); - canvas.ClipRect(new SKRect(((SKRect)(ref val2)).Left, ((SKRect)(ref val2)).Top, ((SKRect)(ref val2)).Right, ((SKRect)(ref val2)).Top + num2), (SKClipOperation)1, false); - canvas.Translate(((SKRect)(ref val2)).Left, ((SKRect)(ref val2)).Top); - SKRect bounds2 = default(SKRect); - ((SKRect)(ref bounds2))._002Ector(0f, 0f, FlyoutWidth, num2); - FlyoutHeaderView.Measure(new SKSize(FlyoutWidth, num2)); - FlyoutHeaderView.Arrange(bounds2); - FlyoutHeaderView.Draw(canvas); - canvas.Restore(); - } - float num8 = ((SKRect)(ref val2)).Top + num2; - float num9 = ((SKRect)(ref val2)).Bottom - num3; - canvas.Save(); - canvas.ClipRect(new SKRect(((SKRect)(ref val2)).Left, num8, ((SKRect)(ref val2)).Right, num9), (SKClipOperation)1, false); - SKPaint val4 = new SKPaint - { - TextSize = 14f, - IsAntialias = true - }; - try - { - float num10 = num8 - _flyoutScrollOffset; - for (int i = 0; i < _sections.Count; i++) - { - if (num10 + num4 < num8) - { - num10 += num4; - continue; - } - if (num10 > num9) - { - break; - } - ShellSection shellSection = _sections[i]; - bool flag = i == _selectedSectionIndex; - if (flag) - { - SKPaint val5 = new SKPaint - { - Color = new SKColor((byte)33, (byte)150, (byte)243, (byte)30), - Style = (SKPaintStyle)0 - }; - try - { - SKRect val6 = new SKRect(((SKRect)(ref val2)).Left, num10, ((SKRect)(ref val2)).Right, num10 + num4); - canvas.DrawRect(val6, val5); - } - finally - { - ((IDisposable)val5)?.Dispose(); - } - } - val4.Color = (flag ? NavBarBackgroundColor : FlyoutTextColor); - canvas.DrawText(shellSection.Title, ((SKRect)(ref val2)).Left + 16f, num10 + 30f, val4); - num10 += num4; - } - canvas.Restore(); - SKColor flyoutTextColor; - if (!string.IsNullOrEmpty(FlyoutFooterText)) - { - float num11 = ((SKRect)(ref val2)).Bottom - num3; - SKPaint val7 = new SKPaint(); - flyoutTextColor = FlyoutTextColor; - val7.Color = ((SKColor)(ref flyoutTextColor)).WithAlpha((byte)50); - val7.Style = (SKPaintStyle)1; - val7.StrokeWidth = 1f; - SKPaint val8 = val7; - try - { - canvas.DrawLine(((SKRect)(ref val2)).Left + 16f, num11, ((SKRect)(ref val2)).Right - 16f, num11, val8); - SKPaint val9 = new SKPaint - { - TextSize = 12f - }; - flyoutTextColor = FlyoutTextColor; - val9.Color = ((SKColor)(ref flyoutTextColor)).WithAlpha((byte)150); - val9.IsAntialias = true; - SKPaint val10 = val9; - try - { - SKRect val11 = default(SKRect); - val10.MeasureText(FlyoutFooterText, ref val11); - canvas.DrawText(FlyoutFooterText, ((SKRect)(ref val2)).Left + 16f, num11 + (num3 + ((SKRect)(ref val11)).Height) / 2f, val10); - } - finally - { - ((IDisposable)val10)?.Dispose(); - } - } - finally - { - ((IDisposable)val8)?.Dispose(); - } - } - if (num7 > 0f) - { - SKPaint val12 = new SKPaint(); - flyoutTextColor = FlyoutTextColor; - val12.Color = ((SKColor)(ref flyoutTextColor)).WithAlpha((byte)80); - val12.Style = (SKPaintStyle)0; - val12.IsAntialias = true; - SKPaint val13 = val12; - try - { - float num12 = ((SKRect)(ref val2)).Right - 6f; - float num13 = num6 * (num6 / num5); - float num14 = num8 + _flyoutScrollOffset / num7 * (num6 - num13); - canvas.DrawRoundRect(new SKRoundRect(new SKRect(num12, num14, num12 + 4f, num14 + num13), 2f), val13); - return; - } - finally - { - ((IDisposable)val13)?.Dispose(); - } - } - } - finally - { - ((IDisposable)val4)?.Dispose(); - } - } - finally - { - ((IDisposable)val3)?.Dispose(); - } - } - finally - { - ((IDisposable)val)?.Dispose(); - } - } - - public override SkiaView? HitTest(float x, float y) - { - //IL_0009: Unknown result type (might be due to invalid IL or missing references) - //IL_000e: Unknown result type (might be due to invalid IL or missing references) - //IL_002a: Unknown result type (might be due to invalid IL or missing references) - //IL_002f: Unknown result type (might be due to invalid IL or missing references) - //IL_0051: Unknown result type (might be due to invalid IL or missing references) - //IL_0056: Unknown result type (might be due to invalid IL or missing references) - //IL_0067: Unknown result type (might be due to invalid IL or missing references) - //IL_006c: Unknown result type (might be due to invalid IL or missing references) - //IL_009a: Unknown result type (might be due to invalid IL or missing references) - //IL_009f: Unknown result type (might be due to invalid IL or missing references) - //IL_00bc: Unknown result type (might be due to invalid IL or missing references) - //IL_00c1: Unknown result type (might be due to invalid IL or missing references) - if (base.IsVisible) - { - SKRect bounds = base.Bounds; - if (((SKRect)(ref bounds)).Contains(x, y)) - { - if (_flyoutAnimationProgress > 0f) - { - bounds = base.Bounds; - float num = ((SKRect)(ref bounds)).Left - FlyoutWidth + FlyoutWidth * _flyoutAnimationProgress; - bounds = base.Bounds; - float top = ((SKRect)(ref bounds)).Top; - float num2 = num + FlyoutWidth; - bounds = base.Bounds; - SKRect val = default(SKRect); - ((SKRect)(ref val))._002Ector(num, top, num2, ((SKRect)(ref bounds)).Bottom); - if (((SKRect)(ref val)).Contains(x, y)) - { - return this; - } - if (FlyoutIsPresented) - { - return this; - } - } - if (NavBarIsVisible) - { - bounds = base.Bounds; - if (y < ((SKRect)(ref bounds)).Top + NavBarHeight) - { - return this; - } - } - if (TabBarIsVisible) - { - bounds = base.Bounds; - if (y > ((SKRect)(ref bounds)).Bottom - TabBarHeight) - { - return this; - } - } - if (_currentContent != null) - { - SkiaView skiaView = _currentContent.HitTest(x, y); - if (skiaView != null) - { - return skiaView; - } - } - return this; - } - } - return null; - } - - public override void OnPointerPressed(PointerEventArgs e) - { - //IL_001a: Unknown result type (might be due to invalid IL or missing references) - //IL_001f: Unknown result type (might be due to invalid IL or missing references) - //IL_0041: Unknown result type (might be due to invalid IL or missing references) - //IL_0046: Unknown result type (might be due to invalid IL or missing references) - //IL_0057: Unknown result type (might be due to invalid IL or missing references) - //IL_005c: Unknown result type (might be due to invalid IL or missing references) - //IL_016f: Unknown result type (might be due to invalid IL or missing references) - //IL_0174: Unknown result type (might be due to invalid IL or missing references) - //IL_01db: Unknown result type (might be due to invalid IL or missing references) - //IL_01e0: Unknown result type (might be due to invalid IL or missing references) - //IL_022a: Unknown result type (might be due to invalid IL or missing references) - //IL_022f: Unknown result type (might be due to invalid IL or missing references) - //IL_024e: Unknown result type (might be due to invalid IL or missing references) - //IL_0253: Unknown result type (might be due to invalid IL or missing references) - if (!base.IsEnabled) - { - return; - } - SKRect bounds; - if (_flyoutAnimationProgress > 0f) - { - bounds = base.Bounds; - float num = ((SKRect)(ref bounds)).Left - FlyoutWidth + FlyoutWidth * _flyoutAnimationProgress; - bounds = base.Bounds; - float top = ((SKRect)(ref bounds)).Top; - float num2 = num + FlyoutWidth; - bounds = base.Bounds; - SKRect val = default(SKRect); - ((SKRect)(ref val))._002Ector(num, top, num2, ((SKRect)(ref bounds)).Bottom); - if (((SKRect)(ref val)).Contains(e.X, e.Y)) - { - float num3 = ((FlyoutHeaderView != null) ? FlyoutHeaderHeight : 0f); - if (e.Y < ((SKRect)(ref val)).Top + num3) - { - e.Handled = true; - return; - } - float num4 = ((!string.IsNullOrEmpty(FlyoutFooterText)) ? FlyoutFooterHeight : 0f); - float num5 = ((SKRect)(ref val)).Top + num3 - _flyoutScrollOffset; - float num6 = 48f; - for (int i = 0; i < _sections.Count; i++) - { - if (e.Y >= num5 && e.Y < num5 + num6 && e.Y < ((SKRect)(ref val)).Bottom - num4) - { - NavigateToSection(i); - FlyoutIsPresented = false; - e.Handled = true; - return; - } - num5 += num6; - } - } - else if (FlyoutIsPresented) - { - FlyoutIsPresented = false; - e.Handled = true; - return; - } - } - if (NavBarIsVisible) - { - float y = e.Y; - bounds = base.Bounds; - if (y < ((SKRect)(ref bounds)).Top + NavBarHeight && e.X < 56f) - { - if (CanGoBack) - { - PopAsync(); - e.Handled = true; - return; - } - if (FlyoutBehavior == ShellFlyoutBehavior.Flyout) - { - FlyoutIsPresented = !FlyoutIsPresented; - e.Handled = true; - return; - } - } - } - if (TabBarIsVisible) - { - float y2 = e.Y; - bounds = base.Bounds; - if (y2 > ((SKRect)(ref bounds)).Bottom - TabBarHeight && _selectedSectionIndex >= 0 && _selectedSectionIndex < _sections.Count) - { - ShellSection shellSection = _sections[_selectedSectionIndex]; - bounds = base.Bounds; - float num7 = ((SKRect)(ref bounds)).Width / (float)shellSection.Items.Count; - float x = e.X; - bounds = base.Bounds; - int value = (int)((x - ((SKRect)(ref bounds)).Left) / num7); - value = Math.Clamp(value, 0, shellSection.Items.Count - 1); - if (value != _selectedItemIndex) - { - NavigateToSection(_selectedSectionIndex, value); - } - e.Handled = true; - return; - } - } - base.OnPointerPressed(e); - } - - public override void OnScroll(ScrollEventArgs e) - { - //IL_001c: Unknown result type (might be due to invalid IL or missing references) - //IL_0021: Unknown result type (might be due to invalid IL or missing references) - //IL_0043: Unknown result type (might be due to invalid IL or missing references) - //IL_0048: Unknown result type (might be due to invalid IL or missing references) - //IL_0059: Unknown result type (might be due to invalid IL or missing references) - //IL_005e: Unknown result type (might be due to invalid IL or missing references) - if (FlyoutIsPresented && _flyoutAnimationProgress > 0f) - { - SKRect bounds = base.Bounds; - float num = ((SKRect)(ref bounds)).Left - FlyoutWidth + FlyoutWidth * _flyoutAnimationProgress; - bounds = base.Bounds; - float top = ((SKRect)(ref bounds)).Top; - float num2 = num + FlyoutWidth; - bounds = base.Bounds; - SKRect val = default(SKRect); - ((SKRect)(ref val))._002Ector(num, top, num2, ((SKRect)(ref bounds)).Bottom); - if (((SKRect)(ref val)).Contains(e.X, e.Y)) - { - float num3 = ((FlyoutHeaderView != null) ? FlyoutHeaderHeight : 0f); - float num4 = ((!string.IsNullOrEmpty(FlyoutFooterText)) ? FlyoutFooterHeight : 0f); - float num5 = 48f; - float num6 = (float)_sections.Count * num5; - float num7 = ((SKRect)(ref val)).Height - num3 - num4; - float val2 = Math.Max(0f, num6 - num7); - _flyoutScrollOffset -= e.DeltaY * 30f; - _flyoutScrollOffset = Math.Max(0f, Math.Min(_flyoutScrollOffset, val2)); - Invalidate(); - e.Handled = true; - return; - } - } - base.OnScroll(e); - } + #region BindableProperties + + /// + /// Bindable property for FlyoutIsPresented. + /// + public static readonly BindableProperty FlyoutIsPresentedProperty = + BindableProperty.Create( + nameof(FlyoutIsPresented), + typeof(bool), + typeof(SkiaShell), + false, + BindingMode.TwoWay, + propertyChanged: (b, o, n) => ((SkiaShell)b).OnFlyoutIsPresentedChanged((bool)n)); + + /// + /// Bindable property for FlyoutBehavior. + /// + public static readonly BindableProperty FlyoutBehaviorProperty = + BindableProperty.Create( + nameof(FlyoutBehavior), + typeof(ShellFlyoutBehavior), + typeof(SkiaShell), + ShellFlyoutBehavior.Flyout, + propertyChanged: (b, o, n) => ((SkiaShell)b).Invalidate()); + + /// + /// Bindable property for FlyoutWidth. + /// + public static readonly BindableProperty FlyoutWidthProperty = + BindableProperty.Create( + nameof(FlyoutWidth), + typeof(float), + typeof(SkiaShell), + 280f, + coerceValue: (b, v) => Math.Max(100f, (float)v), + propertyChanged: (b, o, n) => ((SkiaShell)b).Invalidate()); + + /// + /// Bindable property for FlyoutBackgroundColor. + /// + public static readonly BindableProperty FlyoutBackgroundColorProperty = + BindableProperty.Create( + nameof(FlyoutBackgroundColor), + typeof(SKColor), + typeof(SkiaShell), + SKColors.White, + propertyChanged: (b, o, n) => ((SkiaShell)b).Invalidate()); + + /// + /// Bindable property for NavBarBackgroundColor. + /// + public static readonly BindableProperty NavBarBackgroundColorProperty = + BindableProperty.Create( + nameof(NavBarBackgroundColor), + typeof(SKColor), + typeof(SkiaShell), + new SKColor(33, 150, 243), + propertyChanged: (b, o, n) => ((SkiaShell)b).Invalidate()); + + /// + /// Bindable property for NavBarTextColor. + /// + public static readonly BindableProperty NavBarTextColorProperty = + BindableProperty.Create( + nameof(NavBarTextColor), + typeof(SKColor), + typeof(SkiaShell), + SKColors.White, + propertyChanged: (b, o, n) => ((SkiaShell)b).Invalidate()); + + /// + /// Bindable property for NavBarHeight. + /// + public static readonly BindableProperty NavBarHeightProperty = + BindableProperty.Create( + nameof(NavBarHeight), + typeof(float), + typeof(SkiaShell), + 56f, + propertyChanged: (b, o, n) => ((SkiaShell)b).InvalidateMeasure()); + + /// + /// Bindable property for TabBarHeight. + /// + public static readonly BindableProperty TabBarHeightProperty = + BindableProperty.Create( + nameof(TabBarHeight), + typeof(float), + typeof(SkiaShell), + 56f, + propertyChanged: (b, o, n) => ((SkiaShell)b).InvalidateMeasure()); + + /// + /// Bindable property for NavBarIsVisible. + /// + public static readonly BindableProperty NavBarIsVisibleProperty = + BindableProperty.Create( + nameof(NavBarIsVisible), + typeof(bool), + typeof(SkiaShell), + true, + propertyChanged: (b, o, n) => ((SkiaShell)b).InvalidateMeasure()); + + /// + /// Bindable property for TabBarIsVisible. + /// + public static readonly BindableProperty TabBarIsVisibleProperty = + BindableProperty.Create( + nameof(TabBarIsVisible), + typeof(bool), + typeof(SkiaShell), + false, + propertyChanged: (b, o, n) => ((SkiaShell)b).InvalidateMeasure()); + + /// + /// Bindable property for ContentPadding. + /// + public static readonly BindableProperty ContentPaddingProperty = + BindableProperty.Create( + nameof(ContentPadding), + typeof(float), + typeof(SkiaShell), + 16f, + propertyChanged: (b, o, n) => ((SkiaShell)b).InvalidateMeasure()); + + /// + /// Bindable property for Title. + /// + public static readonly BindableProperty TitleProperty = + BindableProperty.Create( + nameof(Title), + typeof(string), + typeof(SkiaShell), + string.Empty, + propertyChanged: (b, o, n) => ((SkiaShell)b).Invalidate()); + + #endregion + + private readonly List _sections = new(); + private SkiaView? _currentContent; + private float _flyoutAnimationProgress = 0f; + private int _selectedSectionIndex = 0; + private int _selectedItemIndex = 0; + + // Navigation stack for push/pop navigation + private readonly Stack<(SkiaView Content, string Title)> _navigationStack = new(); + + private void OnFlyoutIsPresentedChanged(bool newValue) + { + _flyoutAnimationProgress = newValue ? 1f : 0f; + FlyoutIsPresentedChanged?.Invoke(this, EventArgs.Empty); + Invalidate(); + } + + /// + /// Gets or sets whether the flyout is presented. + /// + public bool FlyoutIsPresented + { + get => (bool)GetValue(FlyoutIsPresentedProperty); + set => SetValue(FlyoutIsPresentedProperty, value); + } + + /// + /// Gets or sets the flyout behavior. + /// + public ShellFlyoutBehavior FlyoutBehavior + { + get => (ShellFlyoutBehavior)GetValue(FlyoutBehaviorProperty); + set => SetValue(FlyoutBehaviorProperty, value); + } + + /// + /// Gets or sets the flyout width. + /// + public float FlyoutWidth + { + get => (float)GetValue(FlyoutWidthProperty); + set => SetValue(FlyoutWidthProperty, value); + } + + /// + /// Background color of the flyout. + /// + public SKColor FlyoutBackgroundColor + { + get => (SKColor)GetValue(FlyoutBackgroundColorProperty); + set => SetValue(FlyoutBackgroundColorProperty, value); + } + + /// + /// Background color of the navigation bar. + /// + public SKColor NavBarBackgroundColor + { + get => (SKColor)GetValue(NavBarBackgroundColorProperty); + set => SetValue(NavBarBackgroundColorProperty, value); + } + + /// + /// Text color of the navigation bar title. + /// + public SKColor NavBarTextColor + { + get => (SKColor)GetValue(NavBarTextColorProperty); + set => SetValue(NavBarTextColorProperty, value); + } + + /// + /// Height of the navigation bar. + /// + public float NavBarHeight + { + get => (float)GetValue(NavBarHeightProperty); + set => SetValue(NavBarHeightProperty, value); + } + + /// + /// Height of the tab bar (when using bottom tabs). + /// + public float TabBarHeight + { + get => (float)GetValue(TabBarHeightProperty); + set => SetValue(TabBarHeightProperty, value); + } + + /// + /// Gets or sets whether the navigation bar is visible. + /// + public bool NavBarIsVisible + { + get => (bool)GetValue(NavBarIsVisibleProperty); + set => SetValue(NavBarIsVisibleProperty, value); + } + + /// + /// Gets or sets whether the tab bar is visible. + /// + public bool TabBarIsVisible + { + get => (bool)GetValue(TabBarIsVisibleProperty); + set => SetValue(TabBarIsVisibleProperty, value); + } + + /// + /// Gets or sets the padding applied to page content. + /// Default is 16 pixels on all sides. + /// + public float ContentPadding + { + get => (float)GetValue(ContentPaddingProperty); + set => SetValue(ContentPaddingProperty, value); + } + + /// + /// Current title displayed in the navigation bar. + /// + public string Title + { + get => (string)GetValue(TitleProperty); + set => SetValue(TitleProperty, value); + } + + /// + /// The sections in this shell. + /// + public IReadOnlyList Sections => _sections; + + /// + /// Gets the currently selected section index. + /// + public int CurrentSectionIndex => _selectedSectionIndex; + + /// + /// Event raised when FlyoutIsPresented changes. + /// + public event EventHandler? FlyoutIsPresentedChanged; + + /// + /// Event raised when navigation occurs. + /// + public event EventHandler? Navigated; + + /// + /// Adds a section to the shell. + /// + public void AddSection(ShellSection section) + { + _sections.Add(section); + + if (_sections.Count == 1) + { + NavigateToSection(0, 0); + } + + Invalidate(); + } + + /// + /// Removes a section from the shell. + /// + public void RemoveSection(ShellSection section) + { + _sections.Remove(section); + Invalidate(); + } + + /// + /// Navigates to a specific section and item. + /// + public void NavigateToSection(int sectionIndex, int itemIndex = 0) + { + if (sectionIndex < 0 || sectionIndex >= _sections.Count) return; + + var section = _sections[sectionIndex]; + if (itemIndex < 0 || itemIndex >= section.Items.Count) return; + + // Clear navigation stack when navigating to a new section + _navigationStack.Clear(); + + _selectedSectionIndex = sectionIndex; + _selectedItemIndex = itemIndex; + + var item = section.Items[itemIndex]; + SetCurrentContent(item.Content); + Title = item.Title; + + Navigated?.Invoke(this, new ShellNavigationEventArgs(section, item)); + Invalidate(); + } + + /// + /// Navigates using a URI route. + /// + public void GoToAsync(string route) + { + // Simple route parsing - format: "//section/item" + if (string.IsNullOrEmpty(route)) return; + + var parts = route.TrimStart('/').Split('/'); + if (parts.Length == 0) 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 (parts.Length > 1) + { + // Find matching item + for (int j = 0; j < section.Items.Count; j++) + { + if (section.Items[j].Route.Equals(parts[1], StringComparison.OrdinalIgnoreCase)) + { + NavigateToSection(i, j); + return; + } + } + } + NavigateToSection(i, 0); + return; + } + } + } + + /// + /// Gets whether there are pages on the navigation stack. + /// + public bool CanGoBack => _navigationStack.Count > 0; + + /// + /// Gets the current navigation stack depth. + /// + public int NavigationStackDepth => _navigationStack.Count; + + /// + /// Pushes a new page onto the navigation stack. + /// + public void PushAsync(SkiaView page, string title) + { + // Save current content to stack + if (_currentContent != null) + { + _navigationStack.Push((_currentContent, Title)); + } + + // Set new content + SetCurrentContent(page); + Title = title; + Invalidate(); + } + + /// + /// Pops the current page from the navigation stack. + /// + public bool PopAsync() + { + if (_navigationStack.Count == 0) return false; + + var (previousContent, previousTitle) = _navigationStack.Pop(); + SetCurrentContent(previousContent); + Title = previousTitle; + Invalidate(); + return true; + } + + /// + /// Pops all pages from the navigation stack, returning to the root. + /// + public void PopToRootAsync() + { + if (_navigationStack.Count == 0) return; + + // Get the root content + (SkiaView Content, string Title) root = default; + while (_navigationStack.Count > 0) + { + root = _navigationStack.Pop(); + } + + SetCurrentContent(root.Content); + Title = root.Title; + Invalidate(); + } + + private void SetCurrentContent(SkiaView? content) + { + if (_currentContent != null) + { + RemoveChild(_currentContent); + } + + _currentContent = content; + + if (_currentContent != null) + { + AddChild(_currentContent); + } + } + + protected override SKSize MeasureOverride(SKSize availableSize) + { + // Measure current content with padding accounted for (consistent with ArrangeOverride) + if (_currentContent != null) + { + float contentTop = NavBarIsVisible ? NavBarHeight : 0; + float contentBottom = TabBarIsVisible ? TabBarHeight : 0; + var contentSize = new SKSize( + availableSize.Width - (float)Padding.Left - (float)Padding.Right, + availableSize.Height - contentTop - contentBottom - (float)Padding.Top - (float)Padding.Bottom); + _currentContent.Measure(contentSize); + } + + return availableSize; + } + + protected override SKRect ArrangeOverride(SKRect bounds) + { + Console.WriteLine($"[SkiaShell] ArrangeOverride - bounds={bounds}"); + + // Arrange current content with padding + if (_currentContent != null) + { + float contentTop = bounds.Top + (NavBarIsVisible ? NavBarHeight : 0) + ContentPadding; + float contentBottom = bounds.Bottom - (TabBarIsVisible ? TabBarHeight : 0) - ContentPadding; + var contentBounds = new SKRect( + bounds.Left + ContentPadding, + contentTop, + bounds.Right - ContentPadding, + contentBottom); + Console.WriteLine($"[SkiaShell] Arranging content with bounds={contentBounds}, padding={ContentPadding}"); + _currentContent.Arrange(contentBounds); + } + + return bounds; + } + + protected override void OnDraw(SKCanvas canvas, SKRect bounds) + { + canvas.Save(); + canvas.ClipRect(bounds); + + // Draw content + _currentContent?.Draw(canvas); + + // Draw navigation bar + if (NavBarIsVisible) + { + DrawNavBar(canvas, bounds); + } + + // Draw tab bar + if (TabBarIsVisible) + { + DrawTabBar(canvas, bounds); + } + + // Draw flyout overlay and panel + if (_flyoutAnimationProgress > 0) + { + DrawFlyout(canvas, bounds); + } + + canvas.Restore(); + } + + private void DrawNavBar(SKCanvas canvas, SKRect bounds) + { + var navBarBounds = new SKRect( + bounds.Left, + bounds.Top, + bounds.Right, + bounds.Top + NavBarHeight); + + // Draw background + using var bgPaint = new SKPaint + { + Color = NavBarBackgroundColor, + Style = SKPaintStyle.Fill, + IsAntialias = true + }; + canvas.DrawRect(navBarBounds, bgPaint); + + // Draw nav icon (back arrow if can go back, else hamburger menu if flyout enabled) + using var iconPaint = new SKPaint + { + Color = NavBarTextColor, + Style = SKPaintStyle.Stroke, + StrokeWidth = 2, + StrokeCap = SKStrokeCap.Round, + IsAntialias = true + }; + + float iconLeft = navBarBounds.Left + 16; + float iconCenter = navBarBounds.MidY; + + if (CanGoBack) + { + // Draw iOS-style back chevron "<" + using var chevronPaint = new SKPaint + { + Color = NavBarTextColor, + Style = SKPaintStyle.Stroke, + StrokeWidth = 2.5f, + StrokeCap = SKStrokeCap.Round, + StrokeJoin = SKStrokeJoin.Round, + IsAntialias = true + }; + + // Clean chevron pointing left + float chevronX = iconLeft + 6; + float chevronSize = 10; + canvas.DrawLine(chevronX + chevronSize, iconCenter - chevronSize, chevronX, iconCenter, chevronPaint); + canvas.DrawLine(chevronX, iconCenter, chevronX + chevronSize, iconCenter + chevronSize, chevronPaint); + } + else if (FlyoutBehavior == ShellFlyoutBehavior.Flyout) + { + // Draw hamburger menu icon + canvas.DrawLine(iconLeft, iconCenter - 8, iconLeft + 18, iconCenter - 8, iconPaint); + canvas.DrawLine(iconLeft, iconCenter, iconLeft + 18, iconCenter, iconPaint); + canvas.DrawLine(iconLeft, iconCenter + 8, iconLeft + 18, iconCenter + 8, iconPaint); + } + + // Draw title + using var titlePaint = new SKPaint + { + Color = NavBarTextColor, + TextSize = 20f, + IsAntialias = true, + FakeBoldText = true + }; + + float titleX = (CanGoBack || FlyoutBehavior == ShellFlyoutBehavior.Flyout) ? navBarBounds.Left + 56 : navBarBounds.Left + 16; + float titleY = navBarBounds.MidY + 6; + canvas.DrawText(Title, titleX, titleY, titlePaint); + } + + private void DrawTabBar(SKCanvas canvas, SKRect bounds) + { + if (_selectedSectionIndex < 0 || _selectedSectionIndex >= _sections.Count) return; + + var section = _sections[_selectedSectionIndex]; + if (section.Items.Count <= 1) return; + + var tabBarBounds = new SKRect( + bounds.Left, + bounds.Bottom - TabBarHeight, + bounds.Right, + bounds.Bottom); + + // Draw background + using var bgPaint = new SKPaint + { + Color = SKColors.White, + Style = SKPaintStyle.Fill, + IsAntialias = true + }; + canvas.DrawRect(tabBarBounds, bgPaint); + + // Draw top border + using var borderPaint = new SKPaint + { + Color = new SKColor(224, 224, 224), + Style = SKPaintStyle.Stroke, + StrokeWidth = 1 + }; + canvas.DrawLine(tabBarBounds.Left, tabBarBounds.Top, tabBarBounds.Right, tabBarBounds.Top, borderPaint); + + // Draw tabs + float tabWidth = tabBarBounds.Width / section.Items.Count; + + using var textPaint = new SKPaint + { + TextSize = 12f, + IsAntialias = true + }; + + for (int i = 0; i < section.Items.Count; i++) + { + var item = section.Items[i]; + bool isSelected = i == _selectedItemIndex; + + textPaint.Color = isSelected ? NavBarBackgroundColor : new SKColor(117, 117, 117); + + var textBounds = new SKRect(); + textPaint.MeasureText(item.Title, ref textBounds); + + float textX = tabBarBounds.Left + i * tabWidth + tabWidth / 2 - textBounds.MidX; + float textY = tabBarBounds.MidY - textBounds.MidY; + + canvas.DrawText(item.Title, textX, textY, textPaint); + } + } + + private void DrawFlyout(SKCanvas canvas, SKRect bounds) + { + // Draw scrim + using var scrimPaint = new SKPaint + { + Color = new SKColor(0, 0, 0, (byte)(100 * _flyoutAnimationProgress)), + Style = SKPaintStyle.Fill + }; + canvas.DrawRect(bounds, scrimPaint); + + // Draw flyout panel + float flyoutX = bounds.Left - FlyoutWidth + (FlyoutWidth * _flyoutAnimationProgress); + var flyoutBounds = new SKRect( + flyoutX, + bounds.Top, + flyoutX + FlyoutWidth, + bounds.Bottom); + + using var flyoutPaint = new SKPaint + { + Color = FlyoutBackgroundColor, + Style = SKPaintStyle.Fill, + IsAntialias = true + }; + canvas.DrawRect(flyoutBounds, flyoutPaint); + + // Draw flyout items + float itemY = flyoutBounds.Top + 80; + float itemHeight = 48f; + + using var itemTextPaint = new SKPaint + { + TextSize = 14f, + IsAntialias = true + }; + + for (int i = 0; i < _sections.Count; i++) + { + var section = _sections[i]; + bool isSelected = i == _selectedSectionIndex; + + // Draw selection background + if (isSelected) + { + using var selectionPaint = new SKPaint + { + Color = new SKColor(33, 150, 243, 30), + Style = SKPaintStyle.Fill + }; + var selectionRect = new SKRect(flyoutBounds.Left, itemY, flyoutBounds.Right, itemY + itemHeight); + canvas.DrawRect(selectionRect, selectionPaint); + } + + itemTextPaint.Color = isSelected ? NavBarBackgroundColor : new SKColor(33, 33, 33); + canvas.DrawText(section.Title, flyoutBounds.Left + 16, itemY + 30, itemTextPaint); + + itemY += itemHeight; + } + } + + public override SkiaView? HitTest(float x, float y) + { + if (!IsVisible || !Bounds.Contains(x, y)) return null; + + // Check flyout area + if (_flyoutAnimationProgress > 0) + { + float flyoutX = Bounds.Left - FlyoutWidth + (FlyoutWidth * _flyoutAnimationProgress); + var flyoutBounds = new SKRect(flyoutX, Bounds.Top, flyoutX + FlyoutWidth, Bounds.Bottom); + + if (flyoutBounds.Contains(x, y)) + { + return this; // Flyout handles its own hits + } + + // Tap on scrim closes flyout + if (FlyoutIsPresented) + { + return this; + } + } + + // Check nav bar + if (NavBarIsVisible && y < Bounds.Top + NavBarHeight) + { + return this; + } + + // Check tab bar + if (TabBarIsVisible && y > Bounds.Bottom - TabBarHeight) + { + return this; + } + + // Check content + if (_currentContent != null) + { + var hit = _currentContent.HitTest(x, y); + if (hit != null) return hit; + } + + return this; + } + + public override void OnPointerPressed(PointerEventArgs e) + { + if (!IsEnabled) return; + + // Check flyout tap + if (_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)) + { + // Check which section was tapped + float itemY = flyoutBounds.Top + 80; + float itemHeight = 48f; + + for (int i = 0; i < _sections.Count; i++) + { + if (e.Y >= itemY && e.Y < itemY + itemHeight) + { + NavigateToSection(i, 0); + FlyoutIsPresented = false; + e.Handled = true; + return; + } + itemY += itemHeight; + } + } + else if (FlyoutIsPresented) + { + // Tap on scrim + FlyoutIsPresented = false; + e.Handled = true; + return; + } + } + + // Check nav bar icon tap (back button or hamburger menu) + if (NavBarIsVisible && e.Y < Bounds.Top + NavBarHeight && e.X < 56) + { + if (CanGoBack) + { + // Back button pressed + PopAsync(); + e.Handled = true; + return; + } + else if (FlyoutBehavior == ShellFlyoutBehavior.Flyout) + { + // Hamburger menu pressed + FlyoutIsPresented = !FlyoutIsPresented; + e.Handled = true; + return; + } + } + + // Check tab bar tap + if (TabBarIsVisible && e.Y > Bounds.Bottom - TabBarHeight) + { + if (_selectedSectionIndex >= 0 && _selectedSectionIndex < _sections.Count) + { + var section = _sections[_selectedSectionIndex]; + float tabWidth = Bounds.Width / section.Items.Count; + int tappedIndex = (int)((e.X - Bounds.Left) / tabWidth); + tappedIndex = Math.Clamp(tappedIndex, 0, section.Items.Count - 1); + + if (tappedIndex != _selectedItemIndex) + { + NavigateToSection(_selectedSectionIndex, tappedIndex); + } + e.Handled = true; + return; + } + } + + base.OnPointerPressed(e); + } +} + +/// +/// Shell flyout behavior options. +/// +public enum ShellFlyoutBehavior +{ + /// + /// No flyout menu. + /// + Disabled, + + /// + /// Flyout slides over content. + /// + Flyout, + + /// + /// Flyout is always visible (side-by-side layout). + /// + Locked +} + +/// +/// Represents a section in the shell (typically shown in flyout). +/// +public class ShellSection +{ + /// + /// The route identifier for this section. + /// + public string Route { get; set; } = string.Empty; + + /// + /// The display title. + /// + public string Title { get; set; } = string.Empty; + + /// + /// Optional icon path. + /// + public string? IconPath { get; set; } + + /// + /// Items in this section. + /// + public List Items { get; } = new(); +} + +/// +/// Represents content within a shell section. +/// +public class ShellContent +{ + /// + /// The route identifier for this content. + /// + public string Route { get; set; } = string.Empty; + + /// + /// The display title. + /// + public string Title { get; set; } = string.Empty; + + /// + /// Optional icon path. + /// + public string? IconPath { get; set; } + + /// + /// The content view. + /// + public SkiaView? Content { get; set; } +} + +/// +/// Event args for shell navigation events. +/// +public class ShellNavigationEventArgs : EventArgs +{ + public ShellSection Section { get; } + public ShellContent Content { get; } + + public ShellNavigationEventArgs(ShellSection section, ShellContent content) + { + Section = section; + Content = content; + } } diff --git a/Views/SkiaSlider.cs b/Views/SkiaSlider.cs index 1c35e47..9b30e74 100644 --- a/Views/SkiaSlider.cs +++ b/Views/SkiaSlider.cs @@ -1,415 +1,398 @@ -using System; -using Microsoft.Maui.Controls; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using SkiaSharp; namespace Microsoft.Maui.Platform; +/// +/// Skia-rendered slider control with full XAML styling support. +/// public class SkiaSlider : SkiaView { - public static readonly BindableProperty MinimumProperty = BindableProperty.Create("Minimum", typeof(double), typeof(SkiaSlider), (object)0.0, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaSlider)(object)b).OnRangeChanged(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + #region BindableProperties - public static readonly BindableProperty MaximumProperty = BindableProperty.Create("Maximum", typeof(double), typeof(SkiaSlider), (object)100.0, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaSlider)(object)b).OnRangeChanged(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for Minimum. + /// + public static readonly BindableProperty MinimumProperty = + BindableProperty.Create( + nameof(Minimum), + typeof(double), + typeof(SkiaSlider), + 0.0, + propertyChanged: (b, o, n) => ((SkiaSlider)b).OnRangeChanged()); - public static readonly BindableProperty ValueProperty = BindableProperty.Create("Value", typeof(double), typeof(SkiaSlider), (object)0.0, (BindingMode)1, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaSlider)(object)b).OnValuePropertyChanged((double)o, (double)n); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for Maximum. + /// + public static readonly BindableProperty MaximumProperty = + BindableProperty.Create( + nameof(Maximum), + typeof(double), + typeof(SkiaSlider), + 100.0, + propertyChanged: (b, o, n) => ((SkiaSlider)b).OnRangeChanged()); - public static readonly BindableProperty TrackColorProperty = BindableProperty.Create("TrackColor", typeof(SKColor), typeof(SkiaSlider), (object)new SKColor((byte)224, (byte)224, (byte)224), (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaSlider)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for Value. + /// + public static readonly BindableProperty ValueProperty = + BindableProperty.Create( + nameof(Value), + typeof(double), + typeof(SkiaSlider), + 0.0, + BindingMode.TwoWay, + propertyChanged: (b, o, n) => ((SkiaSlider)b).OnValuePropertyChanged((double)o, (double)n)); - public static readonly BindableProperty ActiveTrackColorProperty = BindableProperty.Create("ActiveTrackColor", typeof(SKColor), typeof(SkiaSlider), (object)new SKColor((byte)33, (byte)150, (byte)243), (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaSlider)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for TrackColor. + /// + public static readonly BindableProperty TrackColorProperty = + BindableProperty.Create( + nameof(TrackColor), + typeof(SKColor), + typeof(SkiaSlider), + new SKColor(0xE0, 0xE0, 0xE0), + propertyChanged: (b, o, n) => ((SkiaSlider)b).Invalidate()); - public static readonly BindableProperty ThumbColorProperty = BindableProperty.Create("ThumbColor", typeof(SKColor), typeof(SkiaSlider), (object)new SKColor((byte)33, (byte)150, (byte)243), (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaSlider)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for ActiveTrackColor. + /// + public static readonly BindableProperty ActiveTrackColorProperty = + BindableProperty.Create( + nameof(ActiveTrackColor), + typeof(SKColor), + typeof(SkiaSlider), + new SKColor(0x21, 0x96, 0xF3), + propertyChanged: (b, o, n) => ((SkiaSlider)b).Invalidate()); - public static readonly BindableProperty DisabledColorProperty = BindableProperty.Create("DisabledColor", typeof(SKColor), typeof(SkiaSlider), (object)new SKColor((byte)189, (byte)189, (byte)189), (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaSlider)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for ThumbColor. + /// + public static readonly BindableProperty ThumbColorProperty = + BindableProperty.Create( + nameof(ThumbColor), + typeof(SKColor), + typeof(SkiaSlider), + new SKColor(0x21, 0x96, 0xF3), + propertyChanged: (b, o, n) => ((SkiaSlider)b).Invalidate()); - public static readonly BindableProperty TrackHeightProperty = BindableProperty.Create("TrackHeight", typeof(float), typeof(SkiaSlider), (object)4f, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaSlider)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for DisabledColor. + /// + public static readonly BindableProperty DisabledColorProperty = + BindableProperty.Create( + nameof(DisabledColor), + typeof(SKColor), + typeof(SkiaSlider), + new SKColor(0xBD, 0xBD, 0xBD), + propertyChanged: (b, o, n) => ((SkiaSlider)b).Invalidate()); - public static readonly BindableProperty ThumbRadiusProperty = BindableProperty.Create("ThumbRadius", typeof(float), typeof(SkiaSlider), (object)10f, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaSlider)(object)b).InvalidateMeasure(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for TrackHeight. + /// + public static readonly BindableProperty TrackHeightProperty = + BindableProperty.Create( + nameof(TrackHeight), + typeof(float), + typeof(SkiaSlider), + 4f, + propertyChanged: (b, o, n) => ((SkiaSlider)b).Invalidate()); - private bool _isDragging; + /// + /// Bindable property for ThumbRadius. + /// + public static readonly BindableProperty ThumbRadiusProperty = + BindableProperty.Create( + nameof(ThumbRadius), + typeof(float), + typeof(SkiaSlider), + 10f, + propertyChanged: (b, o, n) => ((SkiaSlider)b).InvalidateMeasure()); - public double Minimum - { - get - { - return (double)((BindableObject)this).GetValue(MinimumProperty); - } - set - { - ((BindableObject)this).SetValue(MinimumProperty, (object)value); - } - } + #endregion - public double Maximum - { - get - { - return (double)((BindableObject)this).GetValue(MaximumProperty); - } - set - { - ((BindableObject)this).SetValue(MaximumProperty, (object)value); - } - } + #region Properties - public double Value - { - get - { - return (double)((BindableObject)this).GetValue(ValueProperty); - } - set - { - ((BindableObject)this).SetValue(ValueProperty, (object)Math.Clamp(value, Minimum, Maximum)); - } - } + /// + /// Gets or sets the minimum value. + /// + public double Minimum + { + get => (double)GetValue(MinimumProperty); + set => SetValue(MinimumProperty, value); + } - public SKColor TrackColor - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKColor)((BindableObject)this).GetValue(TrackColorProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(TrackColorProperty, (object)value); - } - } + /// + /// Gets or sets the maximum value. + /// + public double Maximum + { + get => (double)GetValue(MaximumProperty); + set => SetValue(MaximumProperty, value); + } - public SKColor ActiveTrackColor - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKColor)((BindableObject)this).GetValue(ActiveTrackColorProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(ActiveTrackColorProperty, (object)value); - } - } + /// + /// Gets or sets the current value. + /// + public double Value + { + get => (double)GetValue(ValueProperty); + set => SetValue(ValueProperty, Math.Clamp(value, Minimum, Maximum)); + } - public SKColor ThumbColor - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKColor)((BindableObject)this).GetValue(ThumbColorProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(ThumbColorProperty, (object)value); - } - } + /// + /// Gets or sets the track color. + /// + public SKColor TrackColor + { + get => (SKColor)GetValue(TrackColorProperty); + set => SetValue(TrackColorProperty, value); + } - public SKColor DisabledColor - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKColor)((BindableObject)this).GetValue(DisabledColorProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(DisabledColorProperty, (object)value); - } - } + /// + /// Gets or sets the active track color. + /// + public SKColor ActiveTrackColor + { + get => (SKColor)GetValue(ActiveTrackColorProperty); + set => SetValue(ActiveTrackColorProperty, value); + } - public float TrackHeight - { - get - { - return (float)((BindableObject)this).GetValue(TrackHeightProperty); - } - set - { - ((BindableObject)this).SetValue(TrackHeightProperty, (object)value); - } - } + /// + /// Gets or sets the thumb color. + /// + public SKColor ThumbColor + { + get => (SKColor)GetValue(ThumbColorProperty); + set => SetValue(ThumbColorProperty, value); + } - public float ThumbRadius - { - get - { - return (float)((BindableObject)this).GetValue(ThumbRadiusProperty); - } - set - { - ((BindableObject)this).SetValue(ThumbRadiusProperty, (object)value); - } - } + /// + /// Gets or sets the disabled color. + /// + public SKColor DisabledColor + { + get => (SKColor)GetValue(DisabledColorProperty); + set => SetValue(DisabledColorProperty, value); + } - public event EventHandler? ValueChanged; + /// + /// Gets or sets the track height. + /// + public float TrackHeight + { + get => (float)GetValue(TrackHeightProperty); + set => SetValue(TrackHeightProperty, value); + } - public event EventHandler? DragStarted; + /// + /// Gets or sets the thumb radius. + /// + public float ThumbRadius + { + get => (float)GetValue(ThumbRadiusProperty); + set => SetValue(ThumbRadiusProperty, value); + } - public event EventHandler? DragCompleted; + #endregion - public SkiaSlider() - { - base.IsFocusable = true; - } + private bool _isDragging; - private void OnRangeChanged() - { - double num = Math.Clamp(Value, Minimum, Maximum); - if (Value != num) - { - Value = num; - } - Invalidate(); - } + /// + /// Event raised when the value changes. + /// + public event EventHandler? ValueChanged; - private void OnValuePropertyChanged(double oldValue, double newValue) - { - this.ValueChanged?.Invoke(this, new SliderValueChangedEventArgs(newValue)); - Invalidate(); - } + /// + /// Event raised when drag starts. + /// + public event EventHandler? DragStarted; - protected override void OnDraw(SKCanvas canvas, SKRect bounds) - { - //IL_006a: Unknown result type (might be due to invalid IL or missing references) - //IL_006f: Unknown result type (might be due to invalid IL or missing references) - //IL_0081: Unknown result type (might be due to invalid IL or missing references) - //IL_0079: Unknown result type (might be due to invalid IL or missing references) - //IL_008b: Unknown result type (might be due to invalid IL or missing references) - //IL_0092: Unknown result type (might be due to invalid IL or missing references) - //IL_009b: Expected O, but got Unknown - //IL_00b9: Unknown result type (might be due to invalid IL or missing references) - //IL_00ca: Unknown result type (might be due to invalid IL or missing references) - //IL_00d1: Expected O, but got Unknown - //IL_00eb: Unknown result type (might be due to invalid IL or missing references) - //IL_00f0: Unknown result type (might be due to invalid IL or missing references) - //IL_01cd: Unknown result type (might be due to invalid IL or missing references) - //IL_01d2: Unknown result type (might be due to invalid IL or missing references) - //IL_0173: Unknown result type (might be due to invalid IL or missing references) - //IL_0178: Unknown result type (might be due to invalid IL or missing references) - //IL_017e: Unknown result type (might be due to invalid IL or missing references) - //IL_0188: Unknown result type (might be due to invalid IL or missing references) - //IL_018f: Unknown result type (might be due to invalid IL or missing references) - //IL_01a2: Expected O, but got Unknown - //IL_0102: Unknown result type (might be due to invalid IL or missing references) - //IL_00fa: Unknown result type (might be due to invalid IL or missing references) - //IL_01e4: Unknown result type (might be due to invalid IL or missing references) - //IL_01dc: Unknown result type (might be due to invalid IL or missing references) - //IL_010c: Unknown result type (might be due to invalid IL or missing references) - //IL_0113: Unknown result type (might be due to invalid IL or missing references) - //IL_011c: Expected O, but got Unknown - //IL_01ee: Unknown result type (might be due to invalid IL or missing references) - //IL_01f5: Unknown result type (might be due to invalid IL or missing references) - //IL_01fe: Expected O, but got Unknown - //IL_013b: Unknown result type (might be due to invalid IL or missing references) - //IL_014c: Unknown result type (might be due to invalid IL or missing references) - //IL_0153: Expected O, but got Unknown - //IL_0217: Unknown result type (might be due to invalid IL or missing references) - //IL_021c: Unknown result type (might be due to invalid IL or missing references) - //IL_021e: Unknown result type (might be due to invalid IL or missing references) - //IL_0223: Unknown result type (might be due to invalid IL or missing references) - //IL_0229: Unknown result type (might be due to invalid IL or missing references) - //IL_0233: Unknown result type (might be due to invalid IL or missing references) - //IL_023a: Unknown result type (might be due to invalid IL or missing references) - //IL_0243: Expected O, but got Unknown - float midY = ((SKRect)(ref bounds)).MidY; - float num = ((SKRect)(ref bounds)).Left + ThumbRadius; - float num2 = ((SKRect)(ref bounds)).Right - ThumbRadius; - float num3 = num2 - num; - double num4 = ((Maximum > Minimum) ? ((Value - Minimum) / (Maximum - Minimum)) : 0.0); - float num5 = num + (float)(num4 * (double)num3); - SKPaint val = new SKPaint - { - Color = (base.IsEnabled ? TrackColor : DisabledColor), - IsAntialias = true, - Style = (SKPaintStyle)0 - }; - try - { - SKRoundRect val2 = new SKRoundRect(new SKRect(num, midY - TrackHeight / 2f, num2, midY + TrackHeight / 2f), TrackHeight / 2f); - canvas.DrawRoundRect(val2, val); - if (num4 > 0.0) - { - SKPaint val3 = new SKPaint - { - Color = (base.IsEnabled ? ActiveTrackColor : DisabledColor), - IsAntialias = true, - Style = (SKPaintStyle)0 - }; - try - { - SKRoundRect val4 = new SKRoundRect(new SKRect(num, midY - TrackHeight / 2f, num5, midY + TrackHeight / 2f), TrackHeight / 2f); - canvas.DrawRoundRect(val4, val3); - } - finally - { - ((IDisposable)val3)?.Dispose(); - } - } - if (base.IsEnabled) - { - SKPaint val5 = new SKPaint - { - Color = new SKColor((byte)0, (byte)0, (byte)0, (byte)30), - IsAntialias = true, - MaskFilter = SKMaskFilter.CreateBlur((SKBlurStyle)0, 3f) - }; - try - { - canvas.DrawCircle(num5 + 1f, midY + 2f, ThumbRadius, val5); - } - finally - { - ((IDisposable)val5)?.Dispose(); - } - } - SKPaint val6 = new SKPaint - { - Color = (base.IsEnabled ? ThumbColor : DisabledColor), - IsAntialias = true, - Style = (SKPaintStyle)0 - }; - try - { - canvas.DrawCircle(num5, midY, ThumbRadius, val6); - if (base.IsFocused) - { - SKPaint val7 = new SKPaint(); - SKColor thumbColor = ThumbColor; - val7.Color = ((SKColor)(ref thumbColor)).WithAlpha((byte)60); - val7.IsAntialias = true; - val7.Style = (SKPaintStyle)0; - SKPaint val8 = val7; - try - { - canvas.DrawCircle(num5, midY, ThumbRadius + 8f, val8); - return; - } - finally - { - ((IDisposable)val8)?.Dispose(); - } - } - } - finally - { - ((IDisposable)val6)?.Dispose(); - } - } - finally - { - ((IDisposable)val)?.Dispose(); - } - } + /// + /// Event raised when drag completes. + /// + public event EventHandler? DragCompleted; - public override void OnPointerPressed(PointerEventArgs e) - { - if (base.IsEnabled) - { - _isDragging = true; - UpdateValueFromPosition(e.X); - this.DragStarted?.Invoke(this, EventArgs.Empty); - SkiaVisualStateManager.GoToState(this, "Pressed"); - } - } + public SkiaSlider() + { + IsFocusable = true; + } - public override void OnPointerMoved(PointerEventArgs e) - { - if (base.IsEnabled && _isDragging) - { - UpdateValueFromPosition(e.X); - } - } + private void OnRangeChanged() + { + // Clamp value to new range + var clamped = Math.Clamp(Value, Minimum, Maximum); + if (Value != clamped) + { + Value = clamped; + } + Invalidate(); + } - public override void OnPointerReleased(PointerEventArgs e) - { - if (_isDragging) - { - _isDragging = false; - this.DragCompleted?.Invoke(this, EventArgs.Empty); - SkiaVisualStateManager.GoToState(this, base.IsEnabled ? "Normal" : "Disabled"); - } - } + private void OnValuePropertyChanged(double oldValue, double newValue) + { + ValueChanged?.Invoke(this, new SliderValueChangedEventArgs(newValue)); + Invalidate(); + } - private void UpdateValueFromPosition(float x) - { - //IL_0001: Unknown result type (might be due to invalid IL or missing references) - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - //IL_0017: Unknown result type (might be due to invalid IL or missing references) - //IL_001c: Unknown result type (might be due to invalid IL or missing references) - SKRect bounds = base.Bounds; - float num = ((SKRect)(ref bounds)).Left + ThumbRadius; - bounds = base.Bounds; - float num2 = ((SKRect)(ref bounds)).Right - ThumbRadius - num; - float num3 = Math.Clamp((x - num) / num2, 0f, 1f); - Value = Minimum + (double)num3 * (Maximum - Minimum); - } + protected override void OnDraw(SKCanvas canvas, SKRect bounds) + { + var trackY = bounds.MidY; + var trackLeft = bounds.Left + ThumbRadius; + var trackRight = bounds.Right - ThumbRadius; + var trackWidth = trackRight - trackLeft; - public override void OnKeyDown(KeyEventArgs e) - { - if (base.IsEnabled) - { - double num = (Maximum - Minimum) / 100.0; - switch (e.Key) - { - case Key.Left: - case Key.Down: - Value -= num * 10.0; - e.Handled = true; - break; - case Key.Up: - case Key.Right: - Value += num * 10.0; - e.Handled = true; - break; - case Key.Home: - Value = Minimum; - e.Handled = true; - break; - case Key.End: - Value = Maximum; - e.Handled = true; - break; - } - } - } + var percentage = Maximum > Minimum ? (Value - Minimum) / (Maximum - Minimum) : 0; + var thumbX = trackLeft + (float)(percentage * trackWidth); - protected override void OnEnabledChanged() - { - base.OnEnabledChanged(); - SkiaVisualStateManager.GoToState(this, base.IsEnabled ? "Normal" : "Disabled"); - } + // Draw inactive track + using var inactiveTrackPaint = new SKPaint + { + Color = IsEnabled ? TrackColor : DisabledColor, + IsAntialias = true, + Style = SKPaintStyle.Fill + }; - protected override SKSize MeasureOverride(SKSize availableSize) - { - //IL_0017: Unknown result type (might be due to invalid IL or missing references) - return new SKSize(200f, ThumbRadius * 2f + 16f); - } + var inactiveRect = new SKRoundRect( + new SKRect(trackLeft, trackY - TrackHeight / 2, trackRight, trackY + TrackHeight / 2), + TrackHeight / 2); + canvas.DrawRoundRect(inactiveRect, inactiveTrackPaint); + + // Draw active track + if (percentage > 0) + { + using var activeTrackPaint = new SKPaint + { + Color = IsEnabled ? ActiveTrackColor : DisabledColor, + IsAntialias = true, + Style = SKPaintStyle.Fill + }; + + var activeRect = new SKRoundRect( + new SKRect(trackLeft, trackY - TrackHeight / 2, thumbX, trackY + TrackHeight / 2), + TrackHeight / 2); + canvas.DrawRoundRect(activeRect, activeTrackPaint); + } + + // Draw thumb shadow + if (IsEnabled) + { + using var shadowPaint = new SKPaint + { + Color = new SKColor(0, 0, 0, 30), + IsAntialias = true, + MaskFilter = SKMaskFilter.CreateBlur(SKBlurStyle.Normal, 3) + }; + canvas.DrawCircle(thumbX + 1, trackY + 2, ThumbRadius, shadowPaint); + } + + // Draw thumb + using var thumbPaint = new SKPaint + { + Color = IsEnabled ? ThumbColor : DisabledColor, + IsAntialias = true, + Style = SKPaintStyle.Fill + }; + canvas.DrawCircle(thumbX, trackY, ThumbRadius, thumbPaint); + + // Draw focus ring + if (IsFocused) + { + using var focusPaint = new SKPaint + { + Color = ThumbColor.WithAlpha(60), + IsAntialias = true, + Style = SKPaintStyle.Fill + }; + canvas.DrawCircle(thumbX, trackY, ThumbRadius + 8, focusPaint); + } + } + + public override void OnPointerPressed(PointerEventArgs e) + { + if (!IsEnabled) return; + + _isDragging = true; + UpdateValueFromPosition(e.X); + DragStarted?.Invoke(this, EventArgs.Empty); + SkiaVisualStateManager.GoToState(this, SkiaVisualStateManager.CommonStates.Pressed); + } + + public override void OnPointerMoved(PointerEventArgs e) + { + if (!IsEnabled || !_isDragging) return; + UpdateValueFromPosition(e.X); + } + + public override void OnPointerReleased(PointerEventArgs e) + { + if (_isDragging) + { + _isDragging = false; + DragCompleted?.Invoke(this, EventArgs.Empty); + SkiaVisualStateManager.GoToState(this, IsEnabled ? SkiaVisualStateManager.CommonStates.Normal : SkiaVisualStateManager.CommonStates.Disabled); + } + } + + private void UpdateValueFromPosition(float x) + { + var trackLeft = Bounds.Left + ThumbRadius; + var trackRight = Bounds.Right - ThumbRadius; + var trackWidth = trackRight - trackLeft; + + var percentage = Math.Clamp((x - trackLeft) / trackWidth, 0, 1); + Value = Minimum + percentage * (Maximum - Minimum); + } + + public override void OnKeyDown(KeyEventArgs e) + { + if (!IsEnabled) return; + + var step = (Maximum - Minimum) / 100; // 1% steps + + switch (e.Key) + { + case Key.Left: + case Key.Down: + Value -= step * 10; + e.Handled = true; + break; + case Key.Right: + case Key.Up: + Value += step * 10; + e.Handled = true; + break; + case Key.Home: + Value = Minimum; + e.Handled = true; + break; + case Key.End: + Value = Maximum; + e.Handled = true; + break; + } + } + + protected override void OnEnabledChanged() + { + base.OnEnabledChanged(); + SkiaVisualStateManager.GoToState(this, IsEnabled ? SkiaVisualStateManager.CommonStates.Normal : SkiaVisualStateManager.CommonStates.Disabled); + } + + protected override SKSize MeasureOverride(SKSize availableSize) + { + return new SKSize(200, ThumbRadius * 2 + 16); + } +} + +/// +/// Event args for slider value changed events. +/// +public class SliderValueChangedEventArgs : EventArgs +{ + public double NewValue { get; } + public SliderValueChangedEventArgs(double newValue) => NewValue = newValue; } diff --git a/Views/SkiaStackLayout.cs b/Views/SkiaStackLayout.cs deleted file mode 100644 index e689301..0000000 --- a/Views/SkiaStackLayout.cs +++ /dev/null @@ -1,213 +0,0 @@ -using System; -using System.Linq; -using Microsoft.Maui.Controls; -using SkiaSharp; - -namespace Microsoft.Maui.Platform; - -public class SkiaStackLayout : SkiaLayoutView -{ - public static readonly BindableProperty OrientationProperty = BindableProperty.Create("Orientation", typeof(StackOrientation), typeof(SkiaStackLayout), (object)StackOrientation.Vertical, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaStackLayout)(object)b).InvalidateMeasure(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public StackOrientation Orientation - { - get - { - return (StackOrientation)((BindableObject)this).GetValue(OrientationProperty); - } - set - { - ((BindableObject)this).SetValue(OrientationProperty, (object)value); - } - } - - protected override SKSize MeasureOverride(SKSize availableSize) - { - //IL_0001: Unknown result type (might be due to invalid IL or missing references) - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - //IL_0017: Unknown result type (might be due to invalid IL or missing references) - //IL_001c: Unknown result type (might be due to invalid IL or missing references) - //IL_002e: Unknown result type (might be due to invalid IL or missing references) - //IL_0033: Unknown result type (might be due to invalid IL or missing references) - //IL_0044: Unknown result type (might be due to invalid IL or missing references) - //IL_0049: Unknown result type (might be due to invalid IL or missing references) - //IL_005b: Unknown result type (might be due to invalid IL or missing references) - //IL_0060: Unknown result type (might be due to invalid IL or missing references) - //IL_0071: Unknown result type (might be due to invalid IL or missing references) - //IL_0076: Unknown result type (might be due to invalid IL or missing references) - //IL_0088: Unknown result type (might be due to invalid IL or missing references) - //IL_008d: Unknown result type (might be due to invalid IL or missing references) - //IL_009e: Unknown result type (might be due to invalid IL or missing references) - //IL_00a3: Unknown result type (might be due to invalid IL or missing references) - //IL_014d: Unknown result type (might be due to invalid IL or missing references) - //IL_014f: Unknown result type (might be due to invalid IL or missing references) - //IL_0154: Unknown result type (might be due to invalid IL or missing references) - //IL_024e: Unknown result type (might be due to invalid IL or missing references) - //IL_0235: Unknown result type (might be due to invalid IL or missing references) - SKRect padding = base.Padding; - float num; - if (!float.IsNaN(((SKRect)(ref padding)).Left)) - { - padding = base.Padding; - num = ((SKRect)(ref padding)).Left; - } - else - { - num = 0f; - } - float num2 = num; - padding = base.Padding; - float num3; - if (!float.IsNaN(((SKRect)(ref padding)).Right)) - { - padding = base.Padding; - num3 = ((SKRect)(ref padding)).Right; - } - else - { - num3 = 0f; - } - float num4 = num3; - padding = base.Padding; - float num5; - if (!float.IsNaN(((SKRect)(ref padding)).Top)) - { - padding = base.Padding; - num5 = ((SKRect)(ref padding)).Top; - } - else - { - num5 = 0f; - } - float num6 = num5; - padding = base.Padding; - float num7; - if (!float.IsNaN(((SKRect)(ref padding)).Bottom)) - { - padding = base.Padding; - num7 = ((SKRect)(ref padding)).Bottom; - } - else - { - num7 = 0f; - } - float num8 = num7; - float num9 = ((SKSize)(ref availableSize)).Width - num2 - num4; - float num10 = ((SKSize)(ref availableSize)).Height - num6 - num8; - if (num9 < 0f || float.IsNaN(num9)) - { - num9 = 0f; - } - if (num10 < 0f || float.IsNaN(num10)) - { - num10 = 0f; - } - float num11 = 0f; - float num12 = 0f; - float num13 = 0f; - float num14 = 0f; - SKSize availableSize2 = default(SKSize); - ((SKSize)(ref availableSize2))._002Ector(num9, num10); - foreach (SkiaView child in base.Children) - { - if (child.IsVisible) - { - SKSize val = child.Measure(availableSize2); - float num15 = (float.IsNaN(((SKSize)(ref val)).Width) ? 0f : ((SKSize)(ref val)).Width); - float num16 = (float.IsNaN(((SKSize)(ref val)).Height) ? 0f : ((SKSize)(ref val)).Height); - if (Orientation == StackOrientation.Vertical) - { - num12 += num16; - num13 = Math.Max(num13, num15); - } - else - { - num11 += num15; - num14 = Math.Max(num14, num16); - } - } - } - int num17 = base.Children.Count((SkiaView c) => c.IsVisible); - float num18 = (float)Math.Max(0, num17 - 1) * base.Spacing; - if (Orientation == StackOrientation.Vertical) - { - num12 += num18; - return new SKSize(num13 + num2 + num4, num12 + num6 + num8); - } - num11 += num18; - return new SKSize(num11 + num2 + num4, num14 + num6 + num8); - } - - protected override SKRect ArrangeOverride(SKRect bounds) - { - //IL_0001: Unknown result type (might be due to invalid IL or missing references) - //IL_0002: Unknown result type (might be due to invalid IL or missing references) - //IL_0007: Unknown result type (might be due to invalid IL or missing references) - //IL_008d: Unknown result type (might be due to invalid IL or missing references) - //IL_0092: Unknown result type (might be due to invalid IL or missing references) - //IL_02bc: Unknown result type (might be due to invalid IL or missing references) - //IL_01b2: Unknown result type (might be due to invalid IL or missing references) - //IL_01b7: Unknown result type (might be due to invalid IL or missing references) - //IL_01bb: Unknown result type (might be due to invalid IL or missing references) - //IL_01c2: Expected I4, but got Unknown - //IL_024b: Unknown result type (might be due to invalid IL or missing references) - //IL_0250: Unknown result type (might be due to invalid IL or missing references) - //IL_029b: Unknown result type (might be due to invalid IL or missing references) - SKRect contentBounds = GetContentBounds(bounds); - float num = ((float.IsInfinity(((SKRect)(ref contentBounds)).Width) || float.IsNaN(((SKRect)(ref contentBounds)).Width)) ? 800f : ((SKRect)(ref contentBounds)).Width); - float num2 = ((float.IsInfinity(((SKRect)(ref contentBounds)).Height) || float.IsNaN(((SKRect)(ref contentBounds)).Height)) ? 600f : ((SKRect)(ref contentBounds)).Height); - float num3 = 0f; - SKRect val = default(SKRect); - SKRect bounds2 = default(SKRect); - foreach (SkiaView child in base.Children) - { - if (!child.IsVisible) - { - continue; - } - SKSize desiredSize = child.DesiredSize; - float num4 = ((float.IsNaN(((SKSize)(ref desiredSize)).Width) || float.IsInfinity(((SKSize)(ref desiredSize)).Width)) ? num : ((SKSize)(ref desiredSize)).Width); - float num5 = ((float.IsNaN(((SKSize)(ref desiredSize)).Height) || float.IsInfinity(((SKSize)(ref desiredSize)).Height)) ? num2 : ((SKSize)(ref desiredSize)).Height); - if (Orientation == StackOrientation.Vertical) - { - float num6 = Math.Max(0f, num2 - num3); - float num7 = ((child is SkiaScrollView) ? num6 : Math.Min(num5, (num6 > 0f) ? num6 : num5)); - ((SKRect)(ref val))._002Ector(((SKRect)(ref contentBounds)).Left, ((SKRect)(ref contentBounds)).Top + num3, ((SKRect)(ref contentBounds)).Left + num, ((SKRect)(ref contentBounds)).Top + num3 + num7); - num3 += num7 + base.Spacing; - } - else - { - float num8 = Math.Max(0f, num - num3); - float num9 = ((child is SkiaScrollView) ? num8 : Math.Min(num4, (num8 > 0f) ? num8 : num4)); - float num10 = Math.Min(num5, num2); - float num11 = ((SKRect)(ref contentBounds)).Top; - float num12 = ((SKRect)(ref contentBounds)).Top + num10; - LayoutOptions verticalOptions = child.VerticalOptions; - switch ((int)((LayoutOptions)(ref verticalOptions)).Alignment) - { - case 1: - num11 = ((SKRect)(ref contentBounds)).Top + (num2 - num10) / 2f; - num12 = num11 + num10; - break; - case 2: - num11 = ((SKRect)(ref contentBounds)).Top + num2 - num10; - num12 = ((SKRect)(ref contentBounds)).Top + num2; - break; - case 3: - num11 = ((SKRect)(ref contentBounds)).Top; - num12 = ((SKRect)(ref contentBounds)).Top + num2; - break; - } - ((SKRect)(ref val))._002Ector(((SKRect)(ref contentBounds)).Left + num3, num11, ((SKRect)(ref contentBounds)).Left + num3 + num9, num12); - num3 += num9 + base.Spacing; - } - Thickness margin = child.Margin; - ((SKRect)(ref bounds2))._002Ector(((SKRect)(ref val)).Left + (float)((Thickness)(ref margin)).Left, ((SKRect)(ref val)).Top + (float)((Thickness)(ref margin)).Top, ((SKRect)(ref val)).Right - (float)((Thickness)(ref margin)).Right, ((SKRect)(ref val)).Bottom - (float)((Thickness)(ref margin)).Bottom); - child.Arrange(bounds2); - } - return bounds; - } -} diff --git a/Views/SkiaStepper.cs b/Views/SkiaStepper.cs index e6d0202..7d77f4a 100644 --- a/Views/SkiaStepper.cs +++ b/Views/SkiaStepper.cs @@ -1,439 +1,261 @@ -using System; -using Microsoft.Maui.Controls; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using SkiaSharp; namespace Microsoft.Maui.Platform; +/// +/// Skia-rendered stepper control with increment/decrement buttons. +/// public class SkiaStepper : SkiaView { - public static readonly BindableProperty ValueProperty = BindableProperty.Create("Value", typeof(double), typeof(SkiaStepper), (object)0.0, (BindingMode)1, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaStepper)(object)b).OnValuePropertyChanged((double)o, (double)n); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + #region BindableProperties - public static readonly BindableProperty MinimumProperty = BindableProperty.Create("Minimum", typeof(double), typeof(SkiaStepper), (object)0.0, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaStepper)(object)b).OnRangeChanged(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + public static readonly BindableProperty ValueProperty = + BindableProperty.Create(nameof(Value), typeof(double), typeof(SkiaStepper), 0.0, BindingMode.TwoWay, + propertyChanged: (b, o, n) => ((SkiaStepper)b).OnValuePropertyChanged((double)o, (double)n)); - public static readonly BindableProperty MaximumProperty = BindableProperty.Create("Maximum", typeof(double), typeof(SkiaStepper), (object)100.0, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaStepper)(object)b).OnRangeChanged(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + public static readonly BindableProperty MinimumProperty = + BindableProperty.Create(nameof(Minimum), typeof(double), typeof(SkiaStepper), 0.0, + propertyChanged: (b, o, n) => ((SkiaStepper)b).OnRangeChanged()); - public static readonly BindableProperty IncrementProperty = BindableProperty.Create("Increment", typeof(double), typeof(SkiaStepper), (object)1.0, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)null, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + public static readonly BindableProperty MaximumProperty = + BindableProperty.Create(nameof(Maximum), typeof(double), typeof(SkiaStepper), 100.0, + propertyChanged: (b, o, n) => ((SkiaStepper)b).OnRangeChanged()); - public static readonly BindableProperty ButtonBackgroundColorProperty = BindableProperty.Create("ButtonBackgroundColor", typeof(SKColor), typeof(SkiaStepper), (object)new SKColor((byte)224, (byte)224, (byte)224), (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaStepper)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + public static readonly BindableProperty IncrementProperty = + BindableProperty.Create(nameof(Increment), typeof(double), typeof(SkiaStepper), 1.0); - public static readonly BindableProperty ButtonPressedColorProperty = BindableProperty.Create("ButtonPressedColor", typeof(SKColor), typeof(SkiaStepper), (object)new SKColor((byte)189, (byte)189, (byte)189), (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaStepper)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + public static readonly BindableProperty ButtonBackgroundColorProperty = + BindableProperty.Create(nameof(ButtonBackgroundColor), typeof(SKColor), typeof(SkiaStepper), new SKColor(0xE0, 0xE0, 0xE0), + propertyChanged: (b, o, n) => ((SkiaStepper)b).Invalidate()); - public static readonly BindableProperty ButtonDisabledColorProperty = BindableProperty.Create("ButtonDisabledColor", typeof(SKColor), typeof(SkiaStepper), (object)new SKColor((byte)245, (byte)245, (byte)245), (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaStepper)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + public static readonly BindableProperty ButtonPressedColorProperty = + BindableProperty.Create(nameof(ButtonPressedColor), typeof(SKColor), typeof(SkiaStepper), new SKColor(0xBD, 0xBD, 0xBD), + propertyChanged: (b, o, n) => ((SkiaStepper)b).Invalidate()); - public static readonly BindableProperty BorderColorProperty = BindableProperty.Create("BorderColor", typeof(SKColor), typeof(SkiaStepper), (object)new SKColor((byte)189, (byte)189, (byte)189), (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaStepper)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + public static readonly BindableProperty ButtonDisabledColorProperty = + BindableProperty.Create(nameof(ButtonDisabledColor), typeof(SKColor), typeof(SkiaStepper), new SKColor(0xF5, 0xF5, 0xF5), + propertyChanged: (b, o, n) => ((SkiaStepper)b).Invalidate()); - public static readonly BindableProperty SymbolColorProperty = BindableProperty.Create("SymbolColor", typeof(SKColor), typeof(SkiaStepper), (object)SKColors.Black, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaStepper)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + public static readonly BindableProperty BorderColorProperty = + BindableProperty.Create(nameof(BorderColor), typeof(SKColor), typeof(SkiaStepper), new SKColor(0xBD, 0xBD, 0xBD), + propertyChanged: (b, o, n) => ((SkiaStepper)b).Invalidate()); - public static readonly BindableProperty SymbolDisabledColorProperty = BindableProperty.Create("SymbolDisabledColor", typeof(SKColor), typeof(SkiaStepper), (object)new SKColor((byte)189, (byte)189, (byte)189), (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaStepper)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + public static readonly BindableProperty SymbolColorProperty = + BindableProperty.Create(nameof(SymbolColor), typeof(SKColor), typeof(SkiaStepper), SKColors.Black, + propertyChanged: (b, o, n) => ((SkiaStepper)b).Invalidate()); - public static readonly BindableProperty CornerRadiusProperty = BindableProperty.Create("CornerRadius", typeof(float), typeof(SkiaStepper), (object)4f, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaStepper)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + public static readonly BindableProperty SymbolDisabledColorProperty = + BindableProperty.Create(nameof(SymbolDisabledColor), typeof(SKColor), typeof(SkiaStepper), new SKColor(0xBD, 0xBD, 0xBD), + propertyChanged: (b, o, n) => ((SkiaStepper)b).Invalidate()); - public static readonly BindableProperty ButtonWidthProperty = BindableProperty.Create("ButtonWidth", typeof(float), typeof(SkiaStepper), (object)40f, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaStepper)(object)b).InvalidateMeasure(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + public static readonly BindableProperty CornerRadiusProperty = + BindableProperty.Create(nameof(CornerRadius), typeof(float), typeof(SkiaStepper), 4f, + propertyChanged: (b, o, n) => ((SkiaStepper)b).Invalidate()); - private bool _isMinusPressed; + public static readonly BindableProperty ButtonWidthProperty = + BindableProperty.Create(nameof(ButtonWidth), typeof(float), typeof(SkiaStepper), 40f, + propertyChanged: (b, o, n) => ((SkiaStepper)b).InvalidateMeasure()); - private bool _isPlusPressed; + #endregion - public double Value - { - get - { - return (double)((BindableObject)this).GetValue(ValueProperty); - } - set - { - ((BindableObject)this).SetValue(ValueProperty, (object)Math.Clamp(value, Minimum, Maximum)); - } - } + #region Properties - public double Minimum - { - get - { - return (double)((BindableObject)this).GetValue(MinimumProperty); - } - set - { - ((BindableObject)this).SetValue(MinimumProperty, (object)value); - } - } + public double Value + { + get => (double)GetValue(ValueProperty); + set => SetValue(ValueProperty, Math.Clamp(value, Minimum, Maximum)); + } - public double Maximum - { - get - { - return (double)((BindableObject)this).GetValue(MaximumProperty); - } - set - { - ((BindableObject)this).SetValue(MaximumProperty, (object)value); - } - } + public double Minimum + { + get => (double)GetValue(MinimumProperty); + set => SetValue(MinimumProperty, value); + } - public double Increment - { - get - { - return (double)((BindableObject)this).GetValue(IncrementProperty); - } - set - { - ((BindableObject)this).SetValue(IncrementProperty, (object)Math.Max(0.001, value)); - } - } + public double Maximum + { + get => (double)GetValue(MaximumProperty); + set => SetValue(MaximumProperty, value); + } - public SKColor ButtonBackgroundColor - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKColor)((BindableObject)this).GetValue(ButtonBackgroundColorProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(ButtonBackgroundColorProperty, (object)value); - } - } + public double Increment + { + get => (double)GetValue(IncrementProperty); + set => SetValue(IncrementProperty, Math.Max(0.001, value)); + } - public SKColor ButtonPressedColor - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKColor)((BindableObject)this).GetValue(ButtonPressedColorProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(ButtonPressedColorProperty, (object)value); - } - } + public SKColor ButtonBackgroundColor + { + get => (SKColor)GetValue(ButtonBackgroundColorProperty); + set => SetValue(ButtonBackgroundColorProperty, value); + } - public SKColor ButtonDisabledColor - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKColor)((BindableObject)this).GetValue(ButtonDisabledColorProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(ButtonDisabledColorProperty, (object)value); - } - } + public SKColor ButtonPressedColor + { + get => (SKColor)GetValue(ButtonPressedColorProperty); + set => SetValue(ButtonPressedColorProperty, value); + } - public SKColor BorderColor - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKColor)((BindableObject)this).GetValue(BorderColorProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(BorderColorProperty, (object)value); - } - } + public SKColor ButtonDisabledColor + { + get => (SKColor)GetValue(ButtonDisabledColorProperty); + set => SetValue(ButtonDisabledColorProperty, value); + } - public SKColor SymbolColor - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKColor)((BindableObject)this).GetValue(SymbolColorProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(SymbolColorProperty, (object)value); - } - } + public SKColor BorderColor + { + get => (SKColor)GetValue(BorderColorProperty); + set => SetValue(BorderColorProperty, value); + } - public SKColor SymbolDisabledColor - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKColor)((BindableObject)this).GetValue(SymbolDisabledColorProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(SymbolDisabledColorProperty, (object)value); - } - } + public SKColor SymbolColor + { + get => (SKColor)GetValue(SymbolColorProperty); + set => SetValue(SymbolColorProperty, value); + } - public float CornerRadius - { - get - { - return (float)((BindableObject)this).GetValue(CornerRadiusProperty); - } - set - { - ((BindableObject)this).SetValue(CornerRadiusProperty, (object)value); - } - } + public SKColor SymbolDisabledColor + { + get => (SKColor)GetValue(SymbolDisabledColorProperty); + set => SetValue(SymbolDisabledColorProperty, value); + } - public float ButtonWidth - { - get - { - return (float)((BindableObject)this).GetValue(ButtonWidthProperty); - } - set - { - ((BindableObject)this).SetValue(ButtonWidthProperty, (object)value); - } - } + public float CornerRadius + { + get => (float)GetValue(CornerRadiusProperty); + set => SetValue(CornerRadiusProperty, value); + } - public event EventHandler? ValueChanged; + public float ButtonWidth + { + get => (float)GetValue(ButtonWidthProperty); + set => SetValue(ButtonWidthProperty, value); + } - public SkiaStepper() - { - base.IsFocusable = true; - } + #endregion - private void OnValuePropertyChanged(double oldValue, double newValue) - { - this.ValueChanged?.Invoke(this, EventArgs.Empty); - Invalidate(); - } + private bool _isMinusPressed; + private bool _isPlusPressed; - private void OnRangeChanged() - { - double num = Math.Clamp(Value, Minimum, Maximum); - if (Value != num) - { - Value = num; - } - Invalidate(); - } + public event EventHandler? ValueChanged; - protected override void OnDraw(SKCanvas canvas, SKRect bounds) - { - //IL_0056: Unknown result type (might be due to invalid IL or missing references) - //IL_0072: Unknown result type (might be due to invalid IL or missing references) - //IL_008c: Unknown result type (might be due to invalid IL or missing references) - //IL_0091: Unknown result type (might be due to invalid IL or missing references) - //IL_0093: Unknown result type (might be due to invalid IL or missing references) - //IL_009d: Unknown result type (might be due to invalid IL or missing references) - //IL_00a4: Unknown result type (might be due to invalid IL or missing references) - //IL_00af: Unknown result type (might be due to invalid IL or missing references) - //IL_00b7: Expected O, but got Unknown - //IL_00d3: Unknown result type (might be due to invalid IL or missing references) - //IL_00d8: Unknown result type (might be due to invalid IL or missing references) - //IL_00da: Unknown result type (might be due to invalid IL or missing references) - //IL_00e1: Unknown result type (might be due to invalid IL or missing references) - //IL_00ec: Expected O, but got Unknown - SKRect rect = default(SKRect); - ((SKRect)(ref rect))._002Ector(((SKRect)(ref bounds)).Left, ((SKRect)(ref bounds)).Top, ((SKRect)(ref bounds)).Left + ButtonWidth, ((SKRect)(ref bounds)).Bottom); - SKRect rect2 = default(SKRect); - ((SKRect)(ref rect2))._002Ector(((SKRect)(ref bounds)).Right - ButtonWidth, ((SKRect)(ref bounds)).Top, ((SKRect)(ref bounds)).Right, ((SKRect)(ref bounds)).Bottom); - DrawButton(canvas, rect, "-", _isMinusPressed, !CanDecrement()); - DrawButton(canvas, rect2, "+", _isPlusPressed, !CanIncrement()); - SKPaint val = new SKPaint - { - Color = BorderColor, - Style = (SKPaintStyle)1, - StrokeWidth = 1f, - IsAntialias = true - }; - try - { - SKRect val2 = new SKRect(((SKRect)(ref bounds)).Left, ((SKRect)(ref bounds)).Top, ((SKRect)(ref bounds)).Right, ((SKRect)(ref bounds)).Bottom); - canvas.DrawRoundRect(new SKRoundRect(val2, CornerRadius), val); - float midX = ((SKRect)(ref bounds)).MidX; - canvas.DrawLine(midX, ((SKRect)(ref bounds)).Top, midX, ((SKRect)(ref bounds)).Bottom, val); - } - finally - { - ((IDisposable)val)?.Dispose(); - } - } + public SkiaStepper() + { + IsFocusable = true; + } - private void DrawButton(SKCanvas canvas, SKRect rect, string symbol, bool isPressed, bool isDisabled) - { - //IL_0000: Unknown result type (might be due to invalid IL or missing references) - //IL_0005: Unknown result type (might be due to invalid IL or missing references) - //IL_001f: Unknown result type (might be due to invalid IL or missing references) - //IL_0029: Unknown result type (might be due to invalid IL or missing references) - //IL_0030: Unknown result type (might be due to invalid IL or missing references) - //IL_0038: Expected O, but got Unknown - //IL_0017: Unknown result type (might be due to invalid IL or missing references) - //IL_000f: Unknown result type (might be due to invalid IL or missing references) - //IL_0039: Unknown result type (might be due to invalid IL or missing references) - //IL_0054: Unknown result type (might be due to invalid IL or missing references) - //IL_005a: Expected O, but got Unknown - //IL_005b: Unknown result type (might be due to invalid IL or missing references) - //IL_0060: Unknown result type (might be due to invalid IL or missing references) - //IL_006e: Unknown result type (might be due to invalid IL or missing references) - //IL_0066: Unknown result type (might be due to invalid IL or missing references) - //IL_0078: Unknown result type (might be due to invalid IL or missing references) - //IL_0080: Expected O, but got Unknown - //IL_0082: Unknown result type (might be due to invalid IL or missing references) - SKPaint val = new SKPaint - { - Color = (isDisabled ? ButtonDisabledColor : (isPressed ? ButtonPressedColor : ButtonBackgroundColor)), - Style = (SKPaintStyle)0, - IsAntialias = true - }; - try - { - canvas.DrawRect(rect, val); - SKFont val2 = new SKFont(SKTypeface.Default, 20f, 1f, 0f); - try - { - SKPaint val3 = new SKPaint(val2) - { - Color = (isDisabled ? SymbolDisabledColor : SymbolColor), - IsAntialias = true - }; - try - { - SKRect val4 = default(SKRect); - val3.MeasureText(symbol, ref val4); - canvas.DrawText(symbol, ((SKRect)(ref rect)).MidX - ((SKRect)(ref val4)).MidX, ((SKRect)(ref rect)).MidY - ((SKRect)(ref val4)).MidY, val3); - } - finally - { - ((IDisposable)val3)?.Dispose(); - } - } - finally - { - ((IDisposable)val2)?.Dispose(); - } - } - finally - { - ((IDisposable)val)?.Dispose(); - } - } + private void OnValuePropertyChanged(double oldValue, double newValue) + { + ValueChanged?.Invoke(this, EventArgs.Empty); + Invalidate(); + } - private bool CanIncrement() - { - if (base.IsEnabled) - { - return Value < Maximum; - } - return false; - } + private void OnRangeChanged() + { + var clamped = Math.Clamp(Value, Minimum, Maximum); + if (Value != clamped) + { + Value = clamped; + } + Invalidate(); + } - private bool CanDecrement() - { - if (base.IsEnabled) - { - return Value > Minimum; - } - return false; - } + protected override void OnDraw(SKCanvas canvas, SKRect bounds) + { + var minusRect = new SKRect(bounds.Left, bounds.Top, bounds.Left + ButtonWidth, bounds.Bottom); + var plusRect = new SKRect(bounds.Right - ButtonWidth, bounds.Top, bounds.Right, bounds.Bottom); - public override void OnPointerPressed(PointerEventArgs e) - { - //IL_0042: Unknown result type (might be due to invalid IL or missing references) - //IL_0047: Unknown result type (might be due to invalid IL or missing references) - if (!base.IsEnabled) - { - return; - } - if (e.X < ButtonWidth) - { - _isMinusPressed = true; - if (CanDecrement()) - { - Value -= Increment; - } - } - else - { - float x = e.X; - SKRect bounds = base.Bounds; - if (x > ((SKRect)(ref bounds)).Width - ButtonWidth) - { - _isPlusPressed = true; - if (CanIncrement()) - { - Value += Increment; - } - } - } - Invalidate(); - } + DrawButton(canvas, minusRect, "-", _isMinusPressed, !CanDecrement()); + DrawButton(canvas, plusRect, "+", _isPlusPressed, !CanIncrement()); - public override void OnPointerReleased(PointerEventArgs e) - { - _isMinusPressed = false; - _isPlusPressed = false; - Invalidate(); - } + using var borderPaint = new SKPaint + { + Color = BorderColor, + Style = SKPaintStyle.Stroke, + StrokeWidth = 1, + IsAntialias = true + }; - public override void OnKeyDown(KeyEventArgs e) - { - if (!base.IsEnabled) - { - return; - } - switch (e.Key) - { - case Key.Up: - case Key.Right: - if (CanIncrement()) - { - Value += Increment; - } - e.Handled = true; - break; - case Key.Left: - case Key.Down: - if (CanDecrement()) - { - Value -= Increment; - } - e.Handled = true; - break; - } - } + var totalRect = new SKRect(bounds.Left, bounds.Top, bounds.Right, bounds.Bottom); + canvas.DrawRoundRect(new SKRoundRect(totalRect, CornerRadius), borderPaint); - protected override SKSize MeasureOverride(SKSize availableSize) - { - //IL_0017: Unknown result type (might be due to invalid IL or missing references) - return new SKSize(ButtonWidth * 2f + 1f, 32f); - } + var centerX = bounds.MidX; + canvas.DrawLine(centerX, bounds.Top, centerX, bounds.Bottom, borderPaint); + } + + private void DrawButton(SKCanvas canvas, SKRect rect, string symbol, bool isPressed, bool isDisabled) + { + using var bgPaint = new SKPaint + { + Color = isDisabled ? ButtonDisabledColor : (isPressed ? ButtonPressedColor : ButtonBackgroundColor), + Style = SKPaintStyle.Fill, + IsAntialias = true + }; + canvas.DrawRect(rect, bgPaint); + + using var font = new SKFont(SKTypeface.Default, 20); + using var textPaint = new SKPaint(font) + { + Color = isDisabled ? SymbolDisabledColor : SymbolColor, + IsAntialias = true + }; + + var textBounds = new SKRect(); + textPaint.MeasureText(symbol, ref textBounds); + canvas.DrawText(symbol, rect.MidX - textBounds.MidX, rect.MidY - textBounds.MidY, textPaint); + } + + private bool CanIncrement() => IsEnabled && Value < Maximum; + private bool CanDecrement() => IsEnabled && Value > Minimum; + + public override void OnPointerPressed(PointerEventArgs e) + { + if (!IsEnabled) return; + + if (e.X < ButtonWidth) + { + _isMinusPressed = true; + if (CanDecrement()) Value -= Increment; + } + else if (e.X > Bounds.Width - ButtonWidth) + { + _isPlusPressed = true; + if (CanIncrement()) Value += Increment; + } + Invalidate(); + } + + public override void OnPointerReleased(PointerEventArgs e) + { + _isMinusPressed = false; + _isPlusPressed = false; + Invalidate(); + } + + public override void OnKeyDown(KeyEventArgs e) + { + if (!IsEnabled) return; + + switch (e.Key) + { + case Key.Up: + case Key.Right: + if (CanIncrement()) Value += Increment; + e.Handled = true; + break; + case Key.Down: + case Key.Left: + if (CanDecrement()) Value -= Increment; + e.Handled = true; + break; + } + } + + protected override SKSize MeasureOverride(SKSize availableSize) + { + return new SKSize(ButtonWidth * 2 + 1, 32); + } } diff --git a/Views/SkiaSwipeView.cs b/Views/SkiaSwipeView.cs index 5a28216..e3d3f30 100644 --- a/Views/SkiaSwipeView.cs +++ b/Views/SkiaSwipeView.cs @@ -1,375 +1,469 @@ -using System; -using System.Collections.Generic; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using SkiaSharp; namespace Microsoft.Maui.Platform; +/// +/// A view that supports swipe gestures to reveal actions. +/// public class SkiaSwipeView : SkiaLayoutView { - private SkiaView? _content; + private SkiaView? _content; + private readonly List _leftItems = new(); + private readonly List _rightItems = new(); + private readonly List _topItems = new(); + private readonly List _bottomItems = new(); - private readonly List _leftItems = new List(); + private float _swipeOffset = 0f; + private SwipeDirection _activeDirection = SwipeDirection.None; + private bool _isSwiping = false; + private float _swipeStartX; + private float _swipeStartY; + private float _swipeStartOffset; + private bool _isOpen = false; - private readonly List _rightItems = new List(); + private const float SwipeThreshold = 60f; + private const float VelocityThreshold = 500f; + private float _velocity; + private DateTime _lastMoveTime; + private float _lastMovePosition; - private readonly List _topItems = new List(); + /// + /// Gets or sets the content view. + /// + public SkiaView? Content + { + get => _content; + set + { + if (_content != value) + { + if (_content != null) + { + RemoveChild(_content); + } - private readonly List _bottomItems = new List(); + _content = value; - private float _swipeOffset; + if (_content != null) + { + AddChild(_content); + } - private SwipeDirection _activeDirection; + InvalidateMeasure(); + Invalidate(); + } + } + } - private bool _isSwiping; + /// + /// Gets the left swipe items. + /// + public IList LeftItems => _leftItems; - private float _swipeStartX; + /// + /// Gets the right swipe items. + /// + public IList RightItems => _rightItems; - private float _swipeStartY; + /// + /// Gets the top swipe items. + /// + public IList TopItems => _topItems; - private float _swipeStartOffset; + /// + /// Gets the bottom swipe items. + /// + public IList BottomItems => _bottomItems; - private bool _isOpen; + /// + /// Gets or sets the swipe mode. + /// + public SwipeMode Mode { get; set; } = SwipeMode.Reveal; - private const float SwipeThreshold = 60f; + /// + /// Gets or sets the left swipe threshold. + /// + public float LeftSwipeThreshold { get; set; } = 100f; - private const float VelocityThreshold = 500f; + /// + /// Gets or sets the right swipe threshold. + /// + public float RightSwipeThreshold { get; set; } = 100f; - private float _velocity; + /// + /// Event raised when swipe is started. + /// + public event EventHandler? SwipeStarted; - private DateTime _lastMoveTime; + /// + /// Event raised when swipe ends. + /// + public event EventHandler? SwipeEnded; - private float _lastMovePosition; + /// + /// Opens the swipe view in the specified direction. + /// + public void Open(SwipeDirection direction) + { + _activeDirection = direction; + _isOpen = true; - public SkiaView? Content - { - get - { - return _content; - } - set - { - if (_content != value) - { - if (_content != null) - { - RemoveChild(_content); - } - _content = value; - if (_content != null) - { - AddChild(_content); - } - InvalidateMeasure(); - Invalidate(); - } - } - } + float targetOffset = direction switch + { + SwipeDirection.Left => -RightSwipeThreshold, + SwipeDirection.Right => LeftSwipeThreshold, + _ => 0 + }; - public IList LeftItems => _leftItems; + AnimateTo(targetOffset); + } - public IList RightItems => _rightItems; + /// + /// Closes the swipe view. + /// + public void Close() + { + _isOpen = false; + AnimateTo(0); + } - public IList TopItems => _topItems; + private void AnimateTo(float target) + { + // Simple animation - in production would use proper animation + _swipeOffset = target; + Invalidate(); + } - public IList BottomItems => _bottomItems; + protected override SKSize MeasureOverride(SKSize availableSize) + { + if (_content != null) + { + _content.Measure(availableSize); + } + return availableSize; + } - public SwipeMode Mode { get; set; } + protected override SKRect ArrangeOverride(SKRect bounds) + { + if (_content != null) + { + var contentBounds = new SKRect( + bounds.Left + _swipeOffset, + bounds.Top, + bounds.Right + _swipeOffset, + bounds.Bottom); + _content.Arrange(contentBounds); + } + return bounds; + } - public float LeftSwipeThreshold { get; set; } = 100f; + protected override void OnDraw(SKCanvas canvas, SKRect bounds) + { + canvas.Save(); + canvas.ClipRect(bounds); - public float RightSwipeThreshold { get; set; } = 100f; + // Draw swipe items behind content + if (_swipeOffset > 0) + { + DrawSwipeItems(canvas, bounds, _leftItems, true); + } + else if (_swipeOffset < 0) + { + DrawSwipeItems(canvas, bounds, _rightItems, false); + } - public event EventHandler? SwipeStarted; + // Draw content + _content?.Draw(canvas); - public event EventHandler? SwipeEnded; + canvas.Restore(); + } - public void Open(SwipeDirection direction) - { - _activeDirection = direction; - _isOpen = true; - AnimateTo(direction switch - { - SwipeDirection.Left => 0f - RightSwipeThreshold, - SwipeDirection.Right => LeftSwipeThreshold, - _ => 0f, - }); - } + private void DrawSwipeItems(SKCanvas canvas, SKRect bounds, List items, bool isLeft) + { + if (items.Count == 0) return; - public void Close() - { - _isOpen = false; - AnimateTo(0f); - } + float revealWidth = Math.Abs(_swipeOffset); + float itemWidth = revealWidth / items.Count; - private void AnimateTo(float target) - { - _swipeOffset = target; - Invalidate(); - } + for (int i = 0; i < items.Count; i++) + { + var item = items[i]; + float x = isLeft ? bounds.Left + i * itemWidth : bounds.Right - (items.Count - i) * itemWidth; - protected override SKSize MeasureOverride(SKSize availableSize) - { - //IL_0015: Unknown result type (might be due to invalid IL or missing references) - //IL_000e: Unknown result type (might be due to invalid IL or missing references) - //IL_000f: Unknown result type (might be due to invalid IL or missing references) - if (_content != null) - { - _content.Measure(availableSize); - } - return availableSize; - } + var itemBounds = new SKRect( + x, + bounds.Top, + x + itemWidth, + bounds.Bottom); - protected override SKRect ArrangeOverride(SKRect bounds) - { - //IL_0045: Unknown result type (might be due to invalid IL or missing references) - //IL_003f: Unknown result type (might be due to invalid IL or missing references) - if (_content != null) - { - SKRect bounds2 = default(SKRect); - ((SKRect)(ref bounds2))._002Ector(((SKRect)(ref bounds)).Left + _swipeOffset, ((SKRect)(ref bounds)).Top, ((SKRect)(ref bounds)).Right + _swipeOffset, ((SKRect)(ref bounds)).Bottom); - _content.Arrange(bounds2); - } - return bounds; - } + // Draw background + using var bgPaint = new SKPaint + { + Color = item.BackgroundColor, + Style = SKPaintStyle.Fill + }; + canvas.DrawRect(itemBounds, bgPaint); - protected override void OnDraw(SKCanvas canvas, SKRect bounds) - { - //IL_0008: Unknown result type (might be due to invalid IL or missing references) - //IL_001f: Unknown result type (might be due to invalid IL or missing references) - //IL_003d: Unknown result type (might be due to invalid IL or missing references) - canvas.Save(); - canvas.ClipRect(bounds, (SKClipOperation)1, false); - if (_swipeOffset > 0f) - { - DrawSwipeItems(canvas, bounds, _leftItems, isLeft: true); - } - else if (_swipeOffset < 0f) - { - DrawSwipeItems(canvas, bounds, _rightItems, isLeft: false); - } - _content?.Draw(canvas); - canvas.Restore(); - } + // Draw icon or text + if (!string.IsNullOrEmpty(item.Text)) + { + using var textPaint = new SKPaint + { + Color = item.TextColor, + TextSize = 14f, + IsAntialias = true, + TextAlign = SKTextAlign.Center + }; - private void DrawSwipeItems(SKCanvas canvas, SKRect bounds, List items, bool isLeft) - { - //IL_006b: Unknown result type (might be due to invalid IL or missing references) - //IL_0070: Unknown result type (might be due to invalid IL or missing references) - //IL_0072: Unknown result type (might be due to invalid IL or missing references) - //IL_007c: Unknown result type (might be due to invalid IL or missing references) - //IL_0085: Expected O, but got Unknown - //IL_0086: Unknown result type (might be due to invalid IL or missing references) - //IL_009c: Unknown result type (might be due to invalid IL or missing references) - //IL_00a1: Unknown result type (might be due to invalid IL or missing references) - //IL_00a3: Unknown result type (might be due to invalid IL or missing references) - //IL_00ad: Unknown result type (might be due to invalid IL or missing references) - //IL_00b8: Unknown result type (might be due to invalid IL or missing references) - //IL_00bf: Unknown result type (might be due to invalid IL or missing references) - //IL_00c8: Expected O, but got Unknown - if (items.Count == 0) - { - return; - } - float num = Math.Abs(_swipeOffset) / (float)items.Count; - SKRect val = default(SKRect); - for (int i = 0; i < items.Count; i++) - { - SwipeItem swipeItem = items[i]; - float num2 = (isLeft ? (((SKRect)(ref bounds)).Left + (float)i * num) : (((SKRect)(ref bounds)).Right - (float)(items.Count - i) * num)); - ((SKRect)(ref val))._002Ector(num2, ((SKRect)(ref bounds)).Top, num2 + num, ((SKRect)(ref bounds)).Bottom); - SKPaint val2 = new SKPaint - { - Color = swipeItem.BackgroundColor, - Style = (SKPaintStyle)0 - }; - try - { - canvas.DrawRect(val, val2); - if (!string.IsNullOrEmpty(swipeItem.Text)) - { - SKPaint val3 = new SKPaint - { - Color = swipeItem.TextColor, - TextSize = 14f, - IsAntialias = true, - TextAlign = (SKTextAlign)1 - }; - try - { - float num3 = ((SKRect)(ref val)).MidY + 5f; - canvas.DrawText(swipeItem.Text, ((SKRect)(ref val)).MidX, num3, val3); - } - finally - { - ((IDisposable)val3)?.Dispose(); - } - } - } - finally - { - ((IDisposable)val2)?.Dispose(); - } - } - } + float textY = itemBounds.MidY + 5; + canvas.DrawText(item.Text, itemBounds.MidX, textY, textPaint); + } + } + } - public override SkiaView? HitTest(float x, float y) - { - //IL_0009: Unknown result type (might be due to invalid IL or missing references) - //IL_000e: Unknown result type (might be due to invalid IL or missing references) - //IL_0033: Unknown result type (might be due to invalid IL or missing references) - //IL_0038: Unknown result type (might be due to invalid IL or missing references) - //IL_005a: Unknown result type (might be due to invalid IL or missing references) - //IL_005f: Unknown result type (might be due to invalid IL or missing references) - if (base.IsVisible) - { - SKRect bounds = base.Bounds; - if (((SKRect)(ref bounds)).Contains(x, y)) - { - if (_isOpen) - { - if (_swipeOffset > 0f) - { - bounds = base.Bounds; - if (x < ((SKRect)(ref bounds)).Left + _swipeOffset) - { - return this; - } - } - if (_swipeOffset < 0f) - { - bounds = base.Bounds; - if (x > ((SKRect)(ref bounds)).Right + _swipeOffset) - { - return this; - } - } - } - if (_content != null) - { - SkiaView skiaView = _content.HitTest(x, y); - if (skiaView != null) - { - return skiaView; - } - } - return this; - } - } - return null; - } + public override SkiaView? HitTest(float x, float y) + { + if (!IsVisible || !Bounds.Contains(x, y)) return null; - public override void OnPointerPressed(PointerEventArgs e) - { - //IL_002a: Unknown result type (might be due to invalid IL or missing references) - //IL_002f: Unknown result type (might be due to invalid IL or missing references) - //IL_009f: Unknown result type (might be due to invalid IL or missing references) - //IL_00a4: Unknown result type (might be due to invalid IL or missing references) - if (!base.IsEnabled) - { - return; - } - if (_isOpen) - { - SwipeItem swipeItem = null; - SKRect bounds; - if (_swipeOffset > 0f) - { - float x = e.X; - bounds = base.Bounds; - int num = (int)((x - ((SKRect)(ref bounds)).Left) / (_swipeOffset / (float)_leftItems.Count)); - if (num >= 0 && num < _leftItems.Count) - { - swipeItem = _leftItems[num]; - } - } - else if (_swipeOffset < 0f) - { - float num2 = Math.Abs(_swipeOffset) / (float)_rightItems.Count; - float x2 = e.X; - bounds = base.Bounds; - int num3 = (int)((x2 - (((SKRect)(ref bounds)).Right + _swipeOffset)) / num2); - if (num3 >= 0 && num3 < _rightItems.Count) - { - swipeItem = _rightItems[num3]; - } - } - if (swipeItem != null) - { - swipeItem.OnInvoked(); - Close(); - e.Handled = true; - return; - } - } - _isSwiping = true; - _swipeStartX = e.X; - _swipeStartY = e.Y; - _swipeStartOffset = _swipeOffset; - _lastMovePosition = e.X; - _lastMoveTime = DateTime.UtcNow; - _velocity = 0f; - base.OnPointerPressed(e); - } + // Check if hit is on swipe items + if (_isOpen) + { + if (_swipeOffset > 0 && x < Bounds.Left + _swipeOffset) + { + return this; // Hit on left items + } + else if (_swipeOffset < 0 && x > Bounds.Right + _swipeOffset) + { + return this; // Hit on right items + } + } - public override void OnPointerMoved(PointerEventArgs e) - { - if (!_isSwiping) - { - return; - } - float num = e.X - _swipeStartX; - _ = e.Y; - _ = _swipeStartY; - if (_activeDirection == SwipeDirection.None && Math.Abs(num) > 10f) - { - _activeDirection = ((!(num > 0f)) ? SwipeDirection.Left : SwipeDirection.Right); - this.SwipeStarted?.Invoke(this, new SwipeStartedEventArgs(_activeDirection)); - } - if (_activeDirection == SwipeDirection.Right || _activeDirection == SwipeDirection.Left) - { - _swipeOffset = _swipeStartOffset + num; - float max = ((_leftItems.Count > 0) ? LeftSwipeThreshold : 0f); - float min = ((_rightItems.Count > 0) ? (0f - RightSwipeThreshold) : 0f); - _swipeOffset = Math.Clamp(_swipeOffset, min, max); - DateTime utcNow = DateTime.UtcNow; - float num2 = (float)(utcNow - _lastMoveTime).TotalSeconds; - if (num2 > 0f) - { - _velocity = (e.X - _lastMovePosition) / num2; - } - _lastMovePosition = e.X; - _lastMoveTime = utcNow; - Invalidate(); - e.Handled = true; - } - base.OnPointerMoved(e); - } + if (_content != null) + { + var hit = _content.HitTest(x, y); + if (hit != null) return hit; + } - public override void OnPointerReleased(PointerEventArgs e) - { - if (!_isSwiping) - { - return; - } - _isSwiping = false; - bool flag = false; - if ((!(Math.Abs(_velocity) > 500f)) ? (Math.Abs(_swipeOffset) > 60f) : ((_velocity > 0f && _leftItems.Count > 0) || (_velocity < 0f && _rightItems.Count > 0))) - { - if (_swipeOffset > 0f) - { - Open(SwipeDirection.Right); - } - else - { - Open(SwipeDirection.Left); - } - } - else - { - Close(); - } - this.SwipeEnded?.Invoke(this, new SwipeEndedEventArgs(_activeDirection, _isOpen)); - _activeDirection = SwipeDirection.None; - base.OnPointerReleased(e); - } + return this; + } + + public override void OnPointerPressed(PointerEventArgs e) + { + if (!IsEnabled) return; + + // Check for swipe item tap when open + if (_isOpen) + { + SwipeItem? tappedItem = null; + + if (_swipeOffset > 0) + { + int index = (int)((e.X - Bounds.Left) / (_swipeOffset / _leftItems.Count)); + if (index >= 0 && index < _leftItems.Count) + { + tappedItem = _leftItems[index]; + } + } + else if (_swipeOffset < 0) + { + float itemWidth = Math.Abs(_swipeOffset) / _rightItems.Count; + int index = (int)((e.X - (Bounds.Right + _swipeOffset)) / itemWidth); + if (index >= 0 && index < _rightItems.Count) + { + tappedItem = _rightItems[index]; + } + } + + if (tappedItem != null) + { + tappedItem.OnInvoked(); + Close(); + e.Handled = true; + return; + } + } + + _isSwiping = true; + _swipeStartX = e.X; + _swipeStartY = e.Y; + _swipeStartOffset = _swipeOffset; + _lastMovePosition = e.X; + _lastMoveTime = DateTime.UtcNow; + _velocity = 0; + + base.OnPointerPressed(e); + } + + public override void OnPointerMoved(PointerEventArgs e) + { + if (!_isSwiping) return; + + float deltaX = e.X - _swipeStartX; + float deltaY = e.Y - _swipeStartY; + + // Determine swipe direction + if (_activeDirection == SwipeDirection.None) + { + if (Math.Abs(deltaX) > 10) + { + _activeDirection = deltaX > 0 ? SwipeDirection.Right : SwipeDirection.Left; + SwipeStarted?.Invoke(this, new SwipeStartedEventArgs(_activeDirection)); + } + } + + if (_activeDirection == SwipeDirection.Right || _activeDirection == SwipeDirection.Left) + { + _swipeOffset = _swipeStartOffset + deltaX; + + // Clamp offset based on available items + float maxRight = _leftItems.Count > 0 ? LeftSwipeThreshold : 0; + float maxLeft = _rightItems.Count > 0 ? -RightSwipeThreshold : 0; + _swipeOffset = Math.Clamp(_swipeOffset, maxLeft, maxRight); + + // Calculate velocity + var now = DateTime.UtcNow; + float timeDelta = (float)(now - _lastMoveTime).TotalSeconds; + if (timeDelta > 0) + { + _velocity = (e.X - _lastMovePosition) / timeDelta; + } + _lastMovePosition = e.X; + _lastMoveTime = now; + + Invalidate(); + e.Handled = true; + } + + base.OnPointerMoved(e); + } + + public override void OnPointerReleased(PointerEventArgs e) + { + if (!_isSwiping) return; + + _isSwiping = false; + + // Determine final state + bool shouldOpen = false; + + if (Math.Abs(_velocity) > VelocityThreshold) + { + // Use velocity + shouldOpen = (_velocity > 0 && _leftItems.Count > 0) || (_velocity < 0 && _rightItems.Count > 0); + } + else + { + // Use threshold + shouldOpen = Math.Abs(_swipeOffset) > SwipeThreshold; + } + + if (shouldOpen) + { + if (_swipeOffset > 0) + { + Open(SwipeDirection.Right); + } + else + { + Open(SwipeDirection.Left); + } + } + else + { + Close(); + } + + SwipeEnded?.Invoke(this, new SwipeEndedEventArgs(_activeDirection, _isOpen)); + _activeDirection = SwipeDirection.None; + + base.OnPointerReleased(e); + } +} + +/// +/// Represents a swipe action item. +/// +public class SwipeItem +{ + /// + /// Gets or sets the text. + /// + public string Text { get; set; } = string.Empty; + + /// + /// Gets or sets the icon source. + /// + public string? IconSource { get; set; } + + /// + /// Gets or sets the background color. + /// + public SKColor BackgroundColor { get; set; } = new SKColor(33, 150, 243); + + /// + /// Gets or sets the text color. + /// + public SKColor TextColor { get; set; } = SKColors.White; + + /// + /// Event raised when the item is invoked. + /// + public event EventHandler? Invoked; + + internal void OnInvoked() + { + Invoked?.Invoke(this, EventArgs.Empty); + } +} + +/// +/// Swipe direction. +/// +public enum SwipeDirection +{ + None, + Left, + Right, + Up, + Down +} + +/// +/// Swipe mode. +/// +public enum SwipeMode +{ + Reveal, + Execute +} + +/// +/// Event args for swipe started. +/// +public class SwipeStartedEventArgs : EventArgs +{ + public SwipeDirection Direction { get; } + + public SwipeStartedEventArgs(SwipeDirection direction) + { + Direction = direction; + } +} + +/// +/// Event args for swipe ended. +/// +public class SwipeEndedEventArgs : EventArgs +{ + public SwipeDirection Direction { get; } + public bool IsOpen { get; } + + public SwipeEndedEventArgs(SwipeDirection direction, bool isOpen) + { + Direction = direction; + IsOpen = isOpen; + } } diff --git a/Views/SkiaSwitch.cs b/Views/SkiaSwitch.cs index a5e33d5..0120307 100644 --- a/Views/SkiaSwitch.cs +++ b/Views/SkiaSwitch.cs @@ -1,343 +1,339 @@ -using System; -using Microsoft.Maui.Controls; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using SkiaSharp; namespace Microsoft.Maui.Platform; +/// +/// Skia-rendered toggle switch control with full XAML styling support. +/// public class SkiaSwitch : SkiaView { - public static readonly BindableProperty IsOnProperty = BindableProperty.Create("IsOn", typeof(bool), typeof(SkiaSwitch), (object)false, (BindingMode)1, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaSwitch)(object)b).OnIsOnChanged(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + #region BindableProperties - public static readonly BindableProperty OnTrackColorProperty = BindableProperty.Create("OnTrackColor", typeof(SKColor), typeof(SkiaSwitch), (object)new SKColor((byte)33, (byte)150, (byte)243), (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaSwitch)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for IsOn. + /// + public static readonly BindableProperty IsOnProperty = + BindableProperty.Create( + nameof(IsOn), + typeof(bool), + typeof(SkiaSwitch), + false, + BindingMode.TwoWay, + propertyChanged: (b, o, n) => ((SkiaSwitch)b).OnIsOnChanged()); - public static readonly BindableProperty OffTrackColorProperty = BindableProperty.Create("OffTrackColor", typeof(SKColor), typeof(SkiaSwitch), (object)new SKColor((byte)158, (byte)158, (byte)158), (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaSwitch)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for OnTrackColor. + /// + public static readonly BindableProperty OnTrackColorProperty = + BindableProperty.Create( + nameof(OnTrackColor), + typeof(SKColor), + typeof(SkiaSwitch), + new SKColor(0x21, 0x96, 0xF3), + propertyChanged: (b, o, n) => ((SkiaSwitch)b).Invalidate()); - public static readonly BindableProperty ThumbColorProperty = BindableProperty.Create("ThumbColor", typeof(SKColor), typeof(SkiaSwitch), (object)SKColors.White, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaSwitch)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for OffTrackColor. + /// + public static readonly BindableProperty OffTrackColorProperty = + BindableProperty.Create( + nameof(OffTrackColor), + typeof(SKColor), + typeof(SkiaSwitch), + new SKColor(0x9E, 0x9E, 0x9E), + propertyChanged: (b, o, n) => ((SkiaSwitch)b).Invalidate()); - public static readonly BindableProperty DisabledColorProperty = BindableProperty.Create("DisabledColor", typeof(SKColor), typeof(SkiaSwitch), (object)new SKColor((byte)189, (byte)189, (byte)189), (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaSwitch)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for ThumbColor. + /// + public static readonly BindableProperty ThumbColorProperty = + BindableProperty.Create( + nameof(ThumbColor), + typeof(SKColor), + typeof(SkiaSwitch), + SKColors.White, + propertyChanged: (b, o, n) => ((SkiaSwitch)b).Invalidate()); - public static readonly BindableProperty TrackWidthProperty = BindableProperty.Create("TrackWidth", typeof(float), typeof(SkiaSwitch), (object)52f, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaSwitch)(object)b).InvalidateMeasure(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for DisabledColor. + /// + public static readonly BindableProperty DisabledColorProperty = + BindableProperty.Create( + nameof(DisabledColor), + typeof(SKColor), + typeof(SkiaSwitch), + new SKColor(0xBD, 0xBD, 0xBD), + propertyChanged: (b, o, n) => ((SkiaSwitch)b).Invalidate()); - public static readonly BindableProperty TrackHeightProperty = BindableProperty.Create("TrackHeight", typeof(float), typeof(SkiaSwitch), (object)32f, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaSwitch)(object)b).InvalidateMeasure(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for TrackWidth. + /// + public static readonly BindableProperty TrackWidthProperty = + BindableProperty.Create( + nameof(TrackWidth), + typeof(float), + typeof(SkiaSwitch), + 52f, + propertyChanged: (b, o, n) => ((SkiaSwitch)b).InvalidateMeasure()); - public static readonly BindableProperty ThumbRadiusProperty = BindableProperty.Create("ThumbRadius", typeof(float), typeof(SkiaSwitch), (object)12f, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaSwitch)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for TrackHeight. + /// + public static readonly BindableProperty TrackHeightProperty = + BindableProperty.Create( + nameof(TrackHeight), + typeof(float), + typeof(SkiaSwitch), + 32f, + propertyChanged: (b, o, n) => ((SkiaSwitch)b).InvalidateMeasure()); - public static readonly BindableProperty ThumbPaddingProperty = BindableProperty.Create("ThumbPadding", typeof(float), typeof(SkiaSwitch), (object)4f, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaSwitch)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + /// + /// Bindable property for ThumbRadius. + /// + public static readonly BindableProperty ThumbRadiusProperty = + BindableProperty.Create( + nameof(ThumbRadius), + typeof(float), + typeof(SkiaSwitch), + 12f, + propertyChanged: (b, o, n) => ((SkiaSwitch)b).Invalidate()); - private float _animationProgress; + /// + /// Bindable property for ThumbPadding. + /// + public static readonly BindableProperty ThumbPaddingProperty = + BindableProperty.Create( + nameof(ThumbPadding), + typeof(float), + typeof(SkiaSwitch), + 4f, + propertyChanged: (b, o, n) => ((SkiaSwitch)b).Invalidate()); - public bool IsOn - { - get - { - return (bool)((BindableObject)this).GetValue(IsOnProperty); - } - set - { - ((BindableObject)this).SetValue(IsOnProperty, (object)value); - } - } + #endregion - public SKColor OnTrackColor - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKColor)((BindableObject)this).GetValue(OnTrackColorProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(OnTrackColorProperty, (object)value); - } - } + #region Properties - public SKColor OffTrackColor - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKColor)((BindableObject)this).GetValue(OffTrackColorProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(OffTrackColorProperty, (object)value); - } - } + /// + /// Gets or sets whether the switch is on. + /// + public bool IsOn + { + get => (bool)GetValue(IsOnProperty); + set => SetValue(IsOnProperty, value); + } - public SKColor ThumbColor - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKColor)((BindableObject)this).GetValue(ThumbColorProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(ThumbColorProperty, (object)value); - } - } + /// + /// Gets or sets the on track color. + /// + public SKColor OnTrackColor + { + get => (SKColor)GetValue(OnTrackColorProperty); + set => SetValue(OnTrackColorProperty, value); + } - public SKColor DisabledColor - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKColor)((BindableObject)this).GetValue(DisabledColorProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(DisabledColorProperty, (object)value); - } - } + /// + /// Gets or sets the off track color. + /// + public SKColor OffTrackColor + { + get => (SKColor)GetValue(OffTrackColorProperty); + set => SetValue(OffTrackColorProperty, value); + } - public float TrackWidth - { - get - { - return (float)((BindableObject)this).GetValue(TrackWidthProperty); - } - set - { - ((BindableObject)this).SetValue(TrackWidthProperty, (object)value); - } - } + /// + /// Gets or sets the thumb color. + /// + public SKColor ThumbColor + { + get => (SKColor)GetValue(ThumbColorProperty); + set => SetValue(ThumbColorProperty, value); + } - public float TrackHeight - { - get - { - return (float)((BindableObject)this).GetValue(TrackHeightProperty); - } - set - { - ((BindableObject)this).SetValue(TrackHeightProperty, (object)value); - } - } + /// + /// Gets or sets the disabled color. + /// + public SKColor DisabledColor + { + get => (SKColor)GetValue(DisabledColorProperty); + set => SetValue(DisabledColorProperty, value); + } - public float ThumbRadius - { - get - { - return (float)((BindableObject)this).GetValue(ThumbRadiusProperty); - } - set - { - ((BindableObject)this).SetValue(ThumbRadiusProperty, (object)value); - } - } + /// + /// Gets or sets the track width. + /// + public float TrackWidth + { + get => (float)GetValue(TrackWidthProperty); + set => SetValue(TrackWidthProperty, value); + } - public float ThumbPadding - { - get - { - return (float)((BindableObject)this).GetValue(ThumbPaddingProperty); - } - set - { - ((BindableObject)this).SetValue(ThumbPaddingProperty, (object)value); - } - } + /// + /// Gets or sets the track height. + /// + public float TrackHeight + { + get => (float)GetValue(TrackHeightProperty); + set => SetValue(TrackHeightProperty, value); + } - public event EventHandler? Toggled; + /// + /// Gets or sets the thumb radius. + /// + public float ThumbRadius + { + get => (float)GetValue(ThumbRadiusProperty); + set => SetValue(ThumbRadiusProperty, value); + } - public SkiaSwitch() - { - base.IsFocusable = true; - } + /// + /// Gets or sets the thumb padding. + /// + public float ThumbPadding + { + get => (float)GetValue(ThumbPaddingProperty); + set => SetValue(ThumbPaddingProperty, value); + } - private void OnIsOnChanged() - { - _animationProgress = (IsOn ? 1f : 0f); - this.Toggled?.Invoke(this, new ToggledEventArgs(IsOn)); - SkiaVisualStateManager.GoToState(this, IsOn ? "On" : "Off"); - Invalidate(); - } + #endregion - protected override void OnDraw(SKCanvas canvas, SKRect bounds) - { - //IL_0067: Unknown result type (might be due to invalid IL or missing references) - //IL_006d: Unknown result type (might be due to invalid IL or missing references) - //IL_0078: Unknown result type (might be due to invalid IL or missing references) - //IL_005f: Unknown result type (might be due to invalid IL or missing references) - //IL_007d: Unknown result type (might be due to invalid IL or missing references) - //IL_007f: Unknown result type (might be due to invalid IL or missing references) - //IL_0084: Unknown result type (might be due to invalid IL or missing references) - //IL_0085: Unknown result type (might be due to invalid IL or missing references) - //IL_008c: Unknown result type (might be due to invalid IL or missing references) - //IL_0093: Unknown result type (might be due to invalid IL or missing references) - //IL_009c: Expected O, but got Unknown - //IL_00ba: Unknown result type (might be due to invalid IL or missing references) - //IL_00cb: Unknown result type (might be due to invalid IL or missing references) - //IL_00d2: Expected O, but got Unknown - //IL_013e: Unknown result type (might be due to invalid IL or missing references) - //IL_0143: Unknown result type (might be due to invalid IL or missing references) - //IL_00e4: Unknown result type (might be due to invalid IL or missing references) - //IL_00e9: Unknown result type (might be due to invalid IL or missing references) - //IL_00ef: Unknown result type (might be due to invalid IL or missing references) - //IL_00f9: Unknown result type (might be due to invalid IL or missing references) - //IL_0100: Unknown result type (might be due to invalid IL or missing references) - //IL_0113: Expected O, but got Unknown - //IL_0163: Unknown result type (might be due to invalid IL or missing references) - //IL_015b: Unknown result type (might be due to invalid IL or missing references) - //IL_016d: Unknown result type (might be due to invalid IL or missing references) - //IL_0174: Unknown result type (might be due to invalid IL or missing references) - //IL_017d: Expected O, but got Unknown - //IL_0196: Unknown result type (might be due to invalid IL or missing references) - //IL_019b: Unknown result type (might be due to invalid IL or missing references) - //IL_019d: Unknown result type (might be due to invalid IL or missing references) - //IL_01a2: Unknown result type (might be due to invalid IL or missing references) - //IL_01a8: Unknown result type (might be due to invalid IL or missing references) - //IL_01b2: Unknown result type (might be due to invalid IL or missing references) - //IL_01b9: Unknown result type (might be due to invalid IL or missing references) - //IL_01c0: Unknown result type (might be due to invalid IL or missing references) - //IL_01cd: Expected O, but got Unknown - //IL_01cf: Unknown result type (might be due to invalid IL or missing references) - //IL_01e0: Unknown result type (might be due to invalid IL or missing references) - //IL_01e7: Expected O, but got Unknown - float midY = ((SKRect)(ref bounds)).MidY; - float num = ((SKRect)(ref bounds)).MidX - TrackWidth / 2f; - float num2 = num + TrackWidth; - float num3 = num + ThumbPadding + ThumbRadius; - float num4 = num2 - ThumbPadding - ThumbRadius; - float num5 = num3 + _animationProgress * (num4 - num3); - SKColor color = (base.IsEnabled ? InterpolateColor(OffTrackColor, OnTrackColor, _animationProgress) : DisabledColor); - SKPaint val = new SKPaint - { - Color = color, - IsAntialias = true, - Style = (SKPaintStyle)0 - }; - try - { - SKRoundRect val2 = new SKRoundRect(new SKRect(num, midY - TrackHeight / 2f, num2, midY + TrackHeight / 2f), TrackHeight / 2f); - canvas.DrawRoundRect(val2, val); - if (base.IsEnabled) - { - SKPaint val3 = new SKPaint - { - Color = new SKColor((byte)0, (byte)0, (byte)0, (byte)40), - IsAntialias = true, - MaskFilter = SKMaskFilter.CreateBlur((SKBlurStyle)0, 2f) - }; - try - { - canvas.DrawCircle(num5 + 1f, midY + 1f, ThumbRadius, val3); - } - finally - { - ((IDisposable)val3)?.Dispose(); - } - } - SKPaint val4 = new SKPaint - { - Color = (SKColor)(base.IsEnabled ? ThumbColor : new SKColor((byte)245, (byte)245, (byte)245)), - IsAntialias = true, - Style = (SKPaintStyle)0 - }; - try - { - canvas.DrawCircle(num5, midY, ThumbRadius, val4); - if (base.IsFocused) - { - SKPaint val5 = new SKPaint(); - SKColor onTrackColor = OnTrackColor; - val5.Color = ((SKColor)(ref onTrackColor)).WithAlpha((byte)60); - val5.IsAntialias = true; - val5.Style = (SKPaintStyle)1; - val5.StrokeWidth = 3f; - SKPaint val6 = val5; - try - { - SKRoundRect val7 = new SKRoundRect(val2.Rect, TrackHeight / 2f); - val7.Inflate(3f, 3f); - canvas.DrawRoundRect(val7, val6); - return; - } - finally - { - ((IDisposable)val6)?.Dispose(); - } - } - } - finally - { - ((IDisposable)val4)?.Dispose(); - } - } - finally - { - ((IDisposable)val)?.Dispose(); - } - } + private float _animationProgress; // 0 = off, 1 = on - private static SKColor InterpolateColor(SKColor from, SKColor to, float t) - { - //IL_0070: Unknown result type (might be due to invalid IL or missing references) - return new SKColor((byte)((float)(int)((SKColor)(ref from)).Red + (float)(((SKColor)(ref to)).Red - ((SKColor)(ref from)).Red) * t), (byte)((float)(int)((SKColor)(ref from)).Green + (float)(((SKColor)(ref to)).Green - ((SKColor)(ref from)).Green) * t), (byte)((float)(int)((SKColor)(ref from)).Blue + (float)(((SKColor)(ref to)).Blue - ((SKColor)(ref from)).Blue) * t), (byte)((float)(int)((SKColor)(ref from)).Alpha + (float)(((SKColor)(ref to)).Alpha - ((SKColor)(ref from)).Alpha) * t)); - } + /// + /// Event raised when the switch is toggled. + /// + public event EventHandler? Toggled; - public override void OnPointerPressed(PointerEventArgs e) - { - if (base.IsEnabled) - { - IsOn = !IsOn; - e.Handled = true; - } - } + public SkiaSwitch() + { + IsFocusable = true; + } - public override void OnPointerReleased(PointerEventArgs e) - { - } + private void OnIsOnChanged() + { + _animationProgress = IsOn ? 1f : 0f; + Toggled?.Invoke(this, new ToggledEventArgs(IsOn)); + SkiaVisualStateManager.GoToState(this, IsOn ? SkiaVisualStateManager.CommonStates.On : SkiaVisualStateManager.CommonStates.Off); + Invalidate(); + } - public override void OnKeyDown(KeyEventArgs e) - { - if (base.IsEnabled && (e.Key == Key.Space || e.Key == Key.Enter)) - { - IsOn = !IsOn; - e.Handled = true; - } - } + protected override void OnDraw(SKCanvas canvas, SKRect bounds) + { + var centerY = bounds.MidY; + var trackLeft = bounds.MidX - TrackWidth / 2; + var trackRight = trackLeft + TrackWidth; - protected override void OnEnabledChanged() - { - base.OnEnabledChanged(); - SkiaVisualStateManager.GoToState(this, base.IsEnabled ? "Normal" : "Disabled"); - } + // Calculate thumb position + var thumbMinX = trackLeft + ThumbPadding + ThumbRadius; + var thumbMaxX = trackRight - ThumbPadding - ThumbRadius; + var thumbX = thumbMinX + _animationProgress * (thumbMaxX - thumbMinX); - protected override SKSize MeasureOverride(SKSize availableSize) - { - //IL_0018: Unknown result type (might be due to invalid IL or missing references) - return new SKSize(TrackWidth + 8f, TrackHeight + 8f); - } + // Interpolate track color + var trackColor = IsEnabled + ? InterpolateColor(OffTrackColor, OnTrackColor, _animationProgress) + : DisabledColor; + + // Draw track + using var trackPaint = new SKPaint + { + Color = trackColor, + IsAntialias = true, + Style = SKPaintStyle.Fill + }; + + var trackRect = new SKRoundRect( + new SKRect(trackLeft, centerY - TrackHeight / 2, trackRight, centerY + TrackHeight / 2), + TrackHeight / 2); + canvas.DrawRoundRect(trackRect, trackPaint); + + // Draw thumb shadow + if (IsEnabled) + { + using var shadowPaint = new SKPaint + { + Color = new SKColor(0, 0, 0, 40), + IsAntialias = true, + MaskFilter = SKMaskFilter.CreateBlur(SKBlurStyle.Normal, 2) + }; + canvas.DrawCircle(thumbX + 1, centerY + 1, ThumbRadius, shadowPaint); + } + + // Draw thumb + using var thumbPaint = new SKPaint + { + Color = IsEnabled ? ThumbColor : new SKColor(0xF5, 0xF5, 0xF5), + IsAntialias = true, + Style = SKPaintStyle.Fill + }; + canvas.DrawCircle(thumbX, centerY, ThumbRadius, thumbPaint); + + // Draw focus ring + if (IsFocused) + { + using var focusPaint = new SKPaint + { + Color = OnTrackColor.WithAlpha(60), + IsAntialias = true, + Style = SKPaintStyle.Stroke, + StrokeWidth = 3 + }; + var focusRect = new SKRoundRect(trackRect.Rect, TrackHeight / 2); + focusRect.Inflate(3, 3); + canvas.DrawRoundRect(focusRect, focusPaint); + } + } + + private static SKColor InterpolateColor(SKColor from, SKColor to, float t) + { + return new SKColor( + (byte)(from.Red + (to.Red - from.Red) * t), + (byte)(from.Green + (to.Green - from.Green) * t), + (byte)(from.Blue + (to.Blue - from.Blue) * t), + (byte)(from.Alpha + (to.Alpha - from.Alpha) * t)); + } + + public override void OnPointerPressed(PointerEventArgs e) + { + if (!IsEnabled) return; + IsOn = !IsOn; + e.Handled = true; + } + + public override void OnPointerReleased(PointerEventArgs e) + { + // Toggle handled in OnPointerPressed + } + + public override void OnKeyDown(KeyEventArgs e) + { + if (!IsEnabled) return; + + if (e.Key == Key.Space || e.Key == Key.Enter) + { + IsOn = !IsOn; + e.Handled = true; + } + } + + protected override void OnEnabledChanged() + { + base.OnEnabledChanged(); + SkiaVisualStateManager.GoToState(this, IsEnabled ? SkiaVisualStateManager.CommonStates.Normal : SkiaVisualStateManager.CommonStates.Disabled); + } + + protected override SKSize MeasureOverride(SKSize availableSize) + { + return new SKSize(TrackWidth + 8, TrackHeight + 8); + } +} + +/// +/// Event args for toggled events. +/// +public class ToggledEventArgs : EventArgs +{ + public bool Value { get; } + public ToggledEventArgs(bool value) => Value = value; } diff --git a/Views/SkiaTabbedPage.cs b/Views/SkiaTabbedPage.cs index facc0b8..7373cb0 100644 --- a/Views/SkiaTabbedPage.cs +++ b/Views/SkiaTabbedPage.cs @@ -1,443 +1,422 @@ -using System; -using System.Collections.Generic; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using SkiaSharp; namespace Microsoft.Maui.Platform; +/// +/// A page that displays tabs for navigation between child pages. +/// public class SkiaTabbedPage : SkiaLayoutView { - private readonly List _tabs = new List(); + private readonly List _tabs = new(); + private int _selectedIndex = 0; + private float _tabBarHeight = 48f; + private bool _tabBarOnBottom = false; - private int _selectedIndex; + /// + /// Gets or sets the height of the tab bar. + /// + public float TabBarHeight + { + get => _tabBarHeight; + set + { + if (_tabBarHeight != value) + { + _tabBarHeight = value; + InvalidateMeasure(); + Invalidate(); + } + } + } - private float _tabBarHeight = 48f; + /// + /// Gets or sets whether the tab bar is positioned at the bottom. + /// + public bool TabBarOnBottom + { + get => _tabBarOnBottom; + set + { + if (_tabBarOnBottom != value) + { + _tabBarOnBottom = value; + Invalidate(); + } + } + } - private bool _tabBarOnBottom; + /// + /// Gets or sets the selected tab index. + /// + public int SelectedIndex + { + get => _selectedIndex; + set + { + if (value >= 0 && value < _tabs.Count && _selectedIndex != value) + { + _selectedIndex = value; + SelectedIndexChanged?.Invoke(this, EventArgs.Empty); + Invalidate(); + } + } + } - public float TabBarHeight - { - get - { - return _tabBarHeight; - } - set - { - if (_tabBarHeight != value) - { - _tabBarHeight = value; - InvalidateMeasure(); - Invalidate(); - } - } - } + /// + /// Gets the currently selected tab. + /// + public TabItem? SelectedTab => _selectedIndex >= 0 && _selectedIndex < _tabs.Count + ? _tabs[_selectedIndex] + : null; - public bool TabBarOnBottom - { - get - { - return _tabBarOnBottom; - } - set - { - if (_tabBarOnBottom != value) - { - _tabBarOnBottom = value; - Invalidate(); - } - } - } + /// + /// Gets the tabs in this page. + /// + public IReadOnlyList Tabs => _tabs; - public int SelectedIndex - { - get - { - return _selectedIndex; - } - set - { - if (value >= 0 && value < _tabs.Count && _selectedIndex != value) - { - _selectedIndex = value; - this.SelectedIndexChanged?.Invoke(this, EventArgs.Empty); - Invalidate(); - } - } - } + /// + /// Background color for the tab bar. + /// + public SKColor TabBarBackgroundColor { get; set; } = new SKColor(33, 150, 243); // Material Blue - public TabItem? SelectedTab - { - get - { - if (_selectedIndex < 0 || _selectedIndex >= _tabs.Count) - { - return null; - } - return _tabs[_selectedIndex]; - } - } + /// + /// Color for selected tab text/icon. + /// + public SKColor SelectedTabColor { get; set; } = SKColors.White; - public IReadOnlyList Tabs => _tabs; + /// + /// Color for unselected tab text/icon. + /// + public SKColor UnselectedTabColor { get; set; } = new SKColor(255, 255, 255, 180); - public SKColor TabBarBackgroundColor { get; set; } = new SKColor((byte)33, (byte)150, (byte)243); + /// + /// Color of the selection indicator. + /// + public SKColor IndicatorColor { get; set; } = SKColors.White; - public SKColor SelectedTabColor { get; set; } = SKColors.White; + /// + /// Height of the selection indicator. + /// + public float IndicatorHeight { get; set; } = 3f; - public SKColor UnselectedTabColor { get; set; } = new SKColor(byte.MaxValue, byte.MaxValue, byte.MaxValue, (byte)180); + /// + /// Event raised when the selected index changes. + /// + public event EventHandler? SelectedIndexChanged; - public SKColor IndicatorColor { get; set; } = SKColors.White; + /// + /// Adds a tab with the specified title and content. + /// + public void AddTab(string title, SkiaView content, string? iconPath = null) + { + var tab = new TabItem + { + Title = title, + Content = content, + IconPath = iconPath + }; - public float IndicatorHeight { get; set; } = 3f; + _tabs.Add(tab); + AddChild(content); - public event EventHandler? SelectedIndexChanged; + if (_tabs.Count == 1) + { + _selectedIndex = 0; + } - public void AddTab(string title, SkiaView content, string? iconPath = null) - { - TabItem item = new TabItem - { - Title = title, - Content = content, - IconPath = iconPath - }; - _tabs.Add(item); - AddChild(content); - if (_tabs.Count == 1) - { - _selectedIndex = 0; - } - InvalidateMeasure(); - Invalidate(); - } + InvalidateMeasure(); + Invalidate(); + } - public void RemoveTab(int index) - { - if (index >= 0 && index < _tabs.Count) - { - TabItem tabItem = _tabs[index]; - _tabs.RemoveAt(index); - RemoveChild(tabItem.Content); - if (_selectedIndex >= _tabs.Count) - { - _selectedIndex = Math.Max(0, _tabs.Count - 1); - } - InvalidateMeasure(); - Invalidate(); - } - } + /// + /// Removes a tab at the specified index. + /// + public void RemoveTab(int index) + { + if (index >= 0 && index < _tabs.Count) + { + var tab = _tabs[index]; + _tabs.RemoveAt(index); + RemoveChild(tab.Content); - public void ClearTabs() - { - foreach (TabItem tab in _tabs) - { - RemoveChild(tab.Content); - } - _tabs.Clear(); - _selectedIndex = 0; - InvalidateMeasure(); - Invalidate(); - } + if (_selectedIndex >= _tabs.Count) + { + _selectedIndex = Math.Max(0, _tabs.Count - 1); + } - protected override SKSize MeasureOverride(SKSize availableSize) - { - //IL_0038: Unknown result type (might be due to invalid IL or missing references) - //IL_0039: Unknown result type (might be due to invalid IL or missing references) - //IL_0058: Unknown result type (might be due to invalid IL or missing references) - float num = ((SKSize)(ref availableSize)).Height - TabBarHeight; - SKSize availableSize2 = default(SKSize); - ((SKSize)(ref availableSize2))._002Ector(((SKSize)(ref availableSize)).Width, num); - foreach (TabItem tab in _tabs) - { - tab.Content.Measure(availableSize2); - } - return availableSize; - } + InvalidateMeasure(); + Invalidate(); + } + } - protected override SKRect ArrangeOverride(SKRect bounds) - { - //IL_0078: Unknown result type (might be due to invalid IL or missing references) - //IL_0097: Unknown result type (might be due to invalid IL or missing references) - SKRect bounds2 = default(SKRect); - if (TabBarOnBottom) - { - ((SKRect)(ref bounds2))._002Ector(((SKRect)(ref bounds)).Left, ((SKRect)(ref bounds)).Top, ((SKRect)(ref bounds)).Right, ((SKRect)(ref bounds)).Bottom - TabBarHeight); - } - else - { - ((SKRect)(ref bounds2))._002Ector(((SKRect)(ref bounds)).Left, ((SKRect)(ref bounds)).Top + TabBarHeight, ((SKRect)(ref bounds)).Right, ((SKRect)(ref bounds)).Bottom); - } - foreach (TabItem tab in _tabs) - { - tab.Content.Arrange(bounds2); - } - return bounds; - } + /// + /// Clears all tabs. + /// + public void ClearTabs() + { + foreach (var tab in _tabs) + { + RemoveChild(tab.Content); + } + _tabs.Clear(); + _selectedIndex = 0; + InvalidateMeasure(); + Invalidate(); + } - protected override void OnDraw(SKCanvas canvas, SKRect bounds) - { - //IL_0008: Unknown result type (might be due to invalid IL or missing references) - canvas.Save(); - canvas.ClipRect(bounds, (SKClipOperation)1, false); - DrawTabBar(canvas); - if (_selectedIndex >= 0 && _selectedIndex < _tabs.Count) - { - _tabs[_selectedIndex].Content.Draw(canvas); - } - canvas.Restore(); - } + protected override SKSize MeasureOverride(SKSize availableSize) + { + // Measure the content area (excluding tab bar) + var contentHeight = availableSize.Height - TabBarHeight; + var contentSize = new SKSize(availableSize.Width, contentHeight); - private void DrawTabBar(SKCanvas canvas) - { - //IL_0057: Unknown result type (might be due to invalid IL or missing references) - //IL_005c: Unknown result type (might be due to invalid IL or missing references) - //IL_0066: Unknown result type (might be due to invalid IL or missing references) - //IL_006b: Unknown result type (might be due to invalid IL or missing references) - //IL_0075: Unknown result type (might be due to invalid IL or missing references) - //IL_007a: Unknown result type (might be due to invalid IL or missing references) - //IL_0084: Unknown result type (might be due to invalid IL or missing references) - //IL_0089: Unknown result type (might be due to invalid IL or missing references) - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - //IL_0010: Unknown result type (might be due to invalid IL or missing references) - //IL_001a: Unknown result type (might be due to invalid IL or missing references) - //IL_001f: Unknown result type (might be due to invalid IL or missing references) - //IL_0030: Unknown result type (might be due to invalid IL or missing references) - //IL_0035: Unknown result type (might be due to invalid IL or missing references) - //IL_003f: Unknown result type (might be due to invalid IL or missing references) - //IL_0044: Unknown result type (might be due to invalid IL or missing references) - //IL_009e: Unknown result type (might be due to invalid IL or missing references) - //IL_00a3: Unknown result type (might be due to invalid IL or missing references) - //IL_00a5: Unknown result type (might be due to invalid IL or missing references) - //IL_00af: Unknown result type (might be due to invalid IL or missing references) - //IL_00b6: Unknown result type (might be due to invalid IL or missing references) - //IL_00be: Expected O, but got Unknown - //IL_00bf: Unknown result type (might be due to invalid IL or missing references) - //IL_00ed: Unknown result type (might be due to invalid IL or missing references) - //IL_00f2: Unknown result type (might be due to invalid IL or missing references) - //IL_00f9: Unknown result type (might be due to invalid IL or missing references) - //IL_0104: Unknown result type (might be due to invalid IL or missing references) - //IL_0110: Expected O, but got Unknown - //IL_0172: Unknown result type (might be due to invalid IL or missing references) - //IL_016a: Unknown result type (might be due to invalid IL or missing references) - //IL_0186: Unknown result type (might be due to invalid IL or missing references) - //IL_020a: Unknown result type (might be due to invalid IL or missing references) - //IL_020f: Unknown result type (might be due to invalid IL or missing references) - //IL_0211: Unknown result type (might be due to invalid IL or missing references) - //IL_021b: Unknown result type (might be due to invalid IL or missing references) - //IL_0222: Unknown result type (might be due to invalid IL or missing references) - //IL_022b: Expected O, but got Unknown - //IL_0270: Unknown result type (might be due to invalid IL or missing references) - //IL_0275: Unknown result type (might be due to invalid IL or missing references) - //IL_0278: Unknown result type (might be due to invalid IL or missing references) - SKRect bounds; - SKRect val = default(SKRect); - if (TabBarOnBottom) - { - bounds = base.Bounds; - float left = ((SKRect)(ref bounds)).Left; - bounds = base.Bounds; - float num = ((SKRect)(ref bounds)).Bottom - TabBarHeight; - bounds = base.Bounds; - float right = ((SKRect)(ref bounds)).Right; - bounds = base.Bounds; - ((SKRect)(ref val))._002Ector(left, num, right, ((SKRect)(ref bounds)).Bottom); - } - else - { - bounds = base.Bounds; - float left2 = ((SKRect)(ref bounds)).Left; - bounds = base.Bounds; - float top = ((SKRect)(ref bounds)).Top; - bounds = base.Bounds; - float right2 = ((SKRect)(ref bounds)).Right; - bounds = base.Bounds; - ((SKRect)(ref val))._002Ector(left2, top, right2, ((SKRect)(ref bounds)).Top + TabBarHeight); - } - SKPaint val2 = new SKPaint - { - Color = TabBarBackgroundColor, - Style = (SKPaintStyle)0, - IsAntialias = true - }; - try - { - canvas.DrawRect(val, val2); - if (_tabs.Count == 0) - { - return; - } - float num2 = ((SKRect)(ref val)).Width / (float)_tabs.Count; - SKPaint val3 = new SKPaint - { - IsAntialias = true, - TextSize = 14f, - Typeface = SKTypeface.Default - }; - try - { - SKRect val4 = default(SKRect); - for (int i = 0; i < _tabs.Count; i++) - { - TabItem tabItem = _tabs[i]; - ((SKRect)(ref val4))._002Ector(((SKRect)(ref val)).Left + (float)i * num2, ((SKRect)(ref val)).Top, ((SKRect)(ref val)).Left + (float)(i + 1) * num2, ((SKRect)(ref val)).Bottom); - bool flag = i == _selectedIndex; - val3.Color = (flag ? SelectedTabColor : UnselectedTabColor); - val3.FakeBoldText = flag; - SKRect val5 = default(SKRect); - val3.MeasureText(tabItem.Title, ref val5); - float num3 = ((SKRect)(ref val4)).MidX - ((SKRect)(ref val5)).MidX; - float num4 = ((SKRect)(ref val4)).MidY - ((SKRect)(ref val5)).MidY; - canvas.DrawText(tabItem.Title, num3, num4, val3); - } - if (_selectedIndex >= 0 && _selectedIndex < _tabs.Count) - { - SKPaint val6 = new SKPaint - { - Color = IndicatorColor, - Style = (SKPaintStyle)0, - IsAntialias = true - }; - try - { - float num5 = ((SKRect)(ref val)).Left + (float)_selectedIndex * num2; - float num6 = (TabBarOnBottom ? ((SKRect)(ref val)).Top : (((SKRect)(ref val)).Bottom - IndicatorHeight)); - SKRect val7 = new SKRect(num5, num6, num5 + num2, num6 + IndicatorHeight); - canvas.DrawRect(val7, val6); - return; - } - finally - { - ((IDisposable)val6)?.Dispose(); - } - } - } - finally - { - ((IDisposable)val3)?.Dispose(); - } - } - finally - { - ((IDisposable)val2)?.Dispose(); - } - } + foreach (var tab in _tabs) + { + tab.Content.Measure(contentSize); + } - public override SkiaView? HitTest(float x, float y) - { - //IL_0009: Unknown result type (might be due to invalid IL or missing references) - //IL_000e: Unknown result type (might be due to invalid IL or missing references) - //IL_006f: Unknown result type (might be due to invalid IL or missing references) - //IL_0074: Unknown result type (might be due to invalid IL or missing references) - //IL_007d: Unknown result type (might be due to invalid IL or missing references) - //IL_0082: Unknown result type (might be due to invalid IL or missing references) - //IL_008b: Unknown result type (might be due to invalid IL or missing references) - //IL_0090: Unknown result type (might be due to invalid IL or missing references) - //IL_0099: Unknown result type (might be due to invalid IL or missing references) - //IL_009e: Unknown result type (might be due to invalid IL or missing references) - //IL_0027: Unknown result type (might be due to invalid IL or missing references) - //IL_002c: Unknown result type (might be due to invalid IL or missing references) - //IL_0035: Unknown result type (might be due to invalid IL or missing references) - //IL_003a: Unknown result type (might be due to invalid IL or missing references) - //IL_004a: Unknown result type (might be due to invalid IL or missing references) - //IL_004f: Unknown result type (might be due to invalid IL or missing references) - //IL_0058: Unknown result type (might be due to invalid IL or missing references) - //IL_005d: Unknown result type (might be due to invalid IL or missing references) - if (base.IsVisible) - { - SKRect bounds = base.Bounds; - if (((SKRect)(ref bounds)).Contains(x, y)) - { - SKRect val = default(SKRect); - if (TabBarOnBottom) - { - bounds = base.Bounds; - float left = ((SKRect)(ref bounds)).Left; - bounds = base.Bounds; - float num = ((SKRect)(ref bounds)).Bottom - TabBarHeight; - bounds = base.Bounds; - float right = ((SKRect)(ref bounds)).Right; - bounds = base.Bounds; - ((SKRect)(ref val))._002Ector(left, num, right, ((SKRect)(ref bounds)).Bottom); - } - else - { - bounds = base.Bounds; - float left2 = ((SKRect)(ref bounds)).Left; - bounds = base.Bounds; - float top = ((SKRect)(ref bounds)).Top; - bounds = base.Bounds; - float right2 = ((SKRect)(ref bounds)).Right; - bounds = base.Bounds; - ((SKRect)(ref val))._002Ector(left2, top, right2, ((SKRect)(ref bounds)).Top + TabBarHeight); - } - if (((SKRect)(ref val)).Contains(x, y)) - { - return this; - } - if (_selectedIndex >= 0 && _selectedIndex < _tabs.Count) - { - SkiaView skiaView = _tabs[_selectedIndex].Content.HitTest(x, y); - if (skiaView != null) - { - return skiaView; - } - } - return this; - } - } - return null; - } + return availableSize; + } - public override void OnPointerPressed(PointerEventArgs e) - { - //IL_005c: Unknown result type (might be due to invalid IL or missing references) - //IL_0061: Unknown result type (might be due to invalid IL or missing references) - //IL_006a: Unknown result type (might be due to invalid IL or missing references) - //IL_006f: Unknown result type (might be due to invalid IL or missing references) - //IL_0078: Unknown result type (might be due to invalid IL or missing references) - //IL_007d: Unknown result type (might be due to invalid IL or missing references) - //IL_0086: Unknown result type (might be due to invalid IL or missing references) - //IL_008b: Unknown result type (might be due to invalid IL or missing references) - //IL_0014: Unknown result type (might be due to invalid IL or missing references) - //IL_0019: Unknown result type (might be due to invalid IL or missing references) - //IL_0022: Unknown result type (might be due to invalid IL or missing references) - //IL_0027: Unknown result type (might be due to invalid IL or missing references) - //IL_0037: Unknown result type (might be due to invalid IL or missing references) - //IL_003c: Unknown result type (might be due to invalid IL or missing references) - //IL_0045: Unknown result type (might be due to invalid IL or missing references) - //IL_004a: Unknown result type (might be due to invalid IL or missing references) - if (base.IsEnabled) - { - SKRect bounds; - SKRect val = default(SKRect); - if (TabBarOnBottom) - { - bounds = base.Bounds; - float left = ((SKRect)(ref bounds)).Left; - bounds = base.Bounds; - float num = ((SKRect)(ref bounds)).Bottom - TabBarHeight; - bounds = base.Bounds; - float right = ((SKRect)(ref bounds)).Right; - bounds = base.Bounds; - ((SKRect)(ref val))._002Ector(left, num, right, ((SKRect)(ref bounds)).Bottom); - } - else - { - bounds = base.Bounds; - float left2 = ((SKRect)(ref bounds)).Left; - bounds = base.Bounds; - float top = ((SKRect)(ref bounds)).Top; - bounds = base.Bounds; - float right2 = ((SKRect)(ref bounds)).Right; - bounds = base.Bounds; - ((SKRect)(ref val))._002Ector(left2, top, right2, ((SKRect)(ref bounds)).Top + TabBarHeight); - } - if (((SKRect)(ref val)).Contains(e.X, e.Y) && _tabs.Count > 0) - { - float num2 = ((SKRect)(ref val)).Width / (float)_tabs.Count; - int value = (int)((e.X - ((SKRect)(ref val)).Left) / num2); - value = Math.Clamp(value, 0, _tabs.Count - 1); - SelectedIndex = value; - e.Handled = true; - } - base.OnPointerPressed(e); - } - } + protected override SKRect ArrangeOverride(SKRect bounds) + { + // Calculate content bounds based on tab bar position + SKRect contentBounds; + if (TabBarOnBottom) + { + contentBounds = new SKRect( + bounds.Left, + bounds.Top, + bounds.Right, + bounds.Bottom - TabBarHeight); + } + else + { + contentBounds = new SKRect( + bounds.Left, + bounds.Top + TabBarHeight, + bounds.Right, + bounds.Bottom); + } + + // Arrange each tab's content to fill the content area + foreach (var tab in _tabs) + { + tab.Content.Arrange(contentBounds); + } + + return bounds; + } + + protected override void OnDraw(SKCanvas canvas, SKRect bounds) + { + canvas.Save(); + canvas.ClipRect(bounds); + + // Draw tab bar background + DrawTabBar(canvas); + + // Draw selected content + if (_selectedIndex >= 0 && _selectedIndex < _tabs.Count) + { + _tabs[_selectedIndex].Content.Draw(canvas); + } + + canvas.Restore(); + } + + private void DrawTabBar(SKCanvas canvas) + { + // Calculate tab bar bounds + SKRect tabBarBounds; + if (TabBarOnBottom) + { + tabBarBounds = new SKRect( + Bounds.Left, + Bounds.Bottom - TabBarHeight, + Bounds.Right, + Bounds.Bottom); + } + else + { + tabBarBounds = new SKRect( + Bounds.Left, + Bounds.Top, + Bounds.Right, + Bounds.Top + TabBarHeight); + } + + // Draw background + using var bgPaint = new SKPaint + { + Color = TabBarBackgroundColor, + Style = SKPaintStyle.Fill, + IsAntialias = true + }; + canvas.DrawRect(tabBarBounds, bgPaint); + + if (_tabs.Count == 0) return; + + // Calculate tab width + float tabWidth = tabBarBounds.Width / _tabs.Count; + + // Draw tabs + using var textPaint = new SKPaint + { + IsAntialias = true, + TextSize = 14f, + Typeface = SKTypeface.Default + }; + + for (int i = 0; i < _tabs.Count; i++) + { + var tab = _tabs[i]; + var tabBounds = new SKRect( + tabBarBounds.Left + i * tabWidth, + tabBarBounds.Top, + tabBarBounds.Left + (i + 1) * tabWidth, + tabBarBounds.Bottom); + + bool isSelected = i == _selectedIndex; + textPaint.Color = isSelected ? SelectedTabColor : UnselectedTabColor; + textPaint.FakeBoldText = isSelected; + + // Draw tab title centered + var textBounds = new SKRect(); + textPaint.MeasureText(tab.Title, ref textBounds); + + float textX = tabBounds.MidX - textBounds.MidX; + float textY = tabBounds.MidY - textBounds.MidY; + + canvas.DrawText(tab.Title, textX, textY, textPaint); + } + + // Draw selection indicator + if (_selectedIndex >= 0 && _selectedIndex < _tabs.Count) + { + using var indicatorPaint = new SKPaint + { + Color = IndicatorColor, + Style = SKPaintStyle.Fill, + IsAntialias = true + }; + + float indicatorLeft = tabBarBounds.Left + _selectedIndex * tabWidth; + float indicatorTop = TabBarOnBottom + ? tabBarBounds.Top + : tabBarBounds.Bottom - IndicatorHeight; + + var indicatorRect = new SKRect( + indicatorLeft, + indicatorTop, + indicatorLeft + tabWidth, + indicatorTop + IndicatorHeight); + + canvas.DrawRect(indicatorRect, indicatorPaint); + } + } + + public override SkiaView? HitTest(float x, float y) + { + if (!IsVisible || !Bounds.Contains(x, y)) return null; + + // Check if hit is in tab bar + SKRect tabBarBounds; + if (TabBarOnBottom) + { + tabBarBounds = new SKRect( + Bounds.Left, + Bounds.Bottom - TabBarHeight, + Bounds.Right, + Bounds.Bottom); + } + else + { + tabBarBounds = new SKRect( + Bounds.Left, + Bounds.Top, + Bounds.Right, + Bounds.Top + TabBarHeight); + } + + if (tabBarBounds.Contains(x, y)) + { + return this; // Tab bar handles its own hits + } + + // Check selected content + if (_selectedIndex >= 0 && _selectedIndex < _tabs.Count) + { + var hit = _tabs[_selectedIndex].Content.HitTest(x, y); + if (hit != null) return hit; + } + + return this; + } + + public override void OnPointerPressed(PointerEventArgs e) + { + if (!IsEnabled) return; + + // Check if click is in tab bar + SKRect tabBarBounds; + if (TabBarOnBottom) + { + tabBarBounds = new SKRect( + Bounds.Left, + Bounds.Bottom - TabBarHeight, + Bounds.Right, + Bounds.Bottom); + } + else + { + tabBarBounds = new SKRect( + Bounds.Left, + Bounds.Top, + Bounds.Right, + Bounds.Top + TabBarHeight); + } + + if (tabBarBounds.Contains(e.X, e.Y) && _tabs.Count > 0) + { + // Calculate which tab was clicked + float tabWidth = tabBarBounds.Width / _tabs.Count; + int clickedIndex = (int)((e.X - tabBarBounds.Left) / tabWidth); + clickedIndex = Math.Clamp(clickedIndex, 0, _tabs.Count - 1); + + SelectedIndex = clickedIndex; + e.Handled = true; + } + + base.OnPointerPressed(e); + } +} + +/// +/// Represents a tab item with title, icon, and content. +/// +public class TabItem +{ + /// + /// The title displayed in the tab. + /// + public string Title { get; set; } = string.Empty; + + /// + /// Optional icon path for the tab. + /// + public string? IconPath { get; set; } + + /// + /// The content view displayed when this tab is selected. + /// + public SkiaView Content { get; set; } = null!; + + /// + /// Optional badge text to display on the tab. + /// + public string? Badge { get; set; } } diff --git a/Views/SkiaTemplatedView.cs b/Views/SkiaTemplatedView.cs index 6139fc5..630a958 100644 --- a/Views/SkiaTemplatedView.cs +++ b/Views/SkiaTemplatedView.cs @@ -1,413 +1,367 @@ -using System; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using Microsoft.Maui.Controls; -using Microsoft.Maui.Controls.Shapes; -using Microsoft.Maui.Graphics; using SkiaSharp; namespace Microsoft.Maui.Platform; +/// +/// Base class for Skia controls that support ControlTemplates. +/// Provides infrastructure for completely redefining control appearance via XAML. +/// public abstract class SkiaTemplatedView : SkiaView { - private SkiaView? _templateRoot; + private SkiaView? _templateRoot; + private bool _templateApplied; - private bool _templateApplied; + #region BindableProperties - public static readonly BindableProperty ControlTemplateProperty = BindableProperty.Create("ControlTemplate", typeof(ControlTemplate), typeof(SkiaTemplatedView), (object)null, (BindingMode)2, (ValidateValueDelegate)null, new BindingPropertyChangedDelegate(OnControlTemplateChanged), (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + public static readonly BindableProperty ControlTemplateProperty = + BindableProperty.Create(nameof(ControlTemplate), typeof(ControlTemplate), typeof(SkiaTemplatedView), null, + propertyChanged: OnControlTemplateChanged); - public ControlTemplate? ControlTemplate - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - //IL_0011: Expected O, but got Unknown - return (ControlTemplate)((BindableObject)this).GetValue(ControlTemplateProperty); - } - set - { - ((BindableObject)this).SetValue(ControlTemplateProperty, (object)value); - } - } + #endregion - protected SkiaView? TemplateRoot => _templateRoot; + #region Properties - protected bool IsTemplateApplied => _templateApplied; + /// + /// Gets or sets the control template that defines the visual appearance. + /// + public ControlTemplate? ControlTemplate + { + get => (ControlTemplate?)GetValue(ControlTemplateProperty); + set => SetValue(ControlTemplateProperty, value); + } - private static void OnControlTemplateChanged(BindableObject bindable, object oldValue, object newValue) - { - //IL_000c: Unknown result type (might be due to invalid IL or missing references) - //IL_0012: Unknown result type (might be due to invalid IL or missing references) - //IL_001c: Expected O, but got Unknown - //IL_001c: Expected O, but got Unknown - if (bindable is SkiaTemplatedView skiaTemplatedView) - { - skiaTemplatedView.OnControlTemplateChanged((ControlTemplate)oldValue, (ControlTemplate)newValue); - } - } + /// + /// Gets the root element created from the ControlTemplate. + /// + protected SkiaView? TemplateRoot => _templateRoot; - protected virtual void OnControlTemplateChanged(ControlTemplate? oldTemplate, ControlTemplate? newTemplate) - { - _templateApplied = false; - _templateRoot = null; - if (newTemplate != null) - { - ApplyTemplate(); - } - InvalidateMeasure(); - } + /// + /// Gets a value indicating whether a template has been applied. + /// + protected bool IsTemplateApplied => _templateApplied; - protected virtual void ApplyTemplate() - { - if (ControlTemplate == null || _templateApplied) - { - return; - } - try - { - object obj = ((ElementTemplate)ControlTemplate).CreateContent(); - Element val = (Element)((obj is Element) ? obj : null); - if (val != null) - { - _templateRoot = ConvertElementToSkiaView(val); - } - else if (obj is SkiaView templateRoot) - { - _templateRoot = templateRoot; - } - if (_templateRoot != null) - { - _templateRoot.Parent = this; - OnTemplateApplied(); - } - _templateApplied = true; - } - catch (Exception) - { - } - } + #endregion - protected virtual void OnTemplateApplied() - { - SkiaContentPresenter skiaContentPresenter = FindTemplateChild("PART_ContentPresenter"); - if (skiaContentPresenter != null) - { - OnContentPresenterFound(skiaContentPresenter); - } - } + private static void OnControlTemplateChanged(BindableObject bindable, object oldValue, object newValue) + { + if (bindable is SkiaTemplatedView view) + { + view.OnControlTemplateChanged((ControlTemplate?)oldValue, (ControlTemplate?)newValue); + } + } - protected virtual void OnContentPresenterFound(SkiaContentPresenter presenter) - { - } + /// + /// Called when the ControlTemplate changes. + /// + protected virtual void OnControlTemplateChanged(ControlTemplate? oldTemplate, ControlTemplate? newTemplate) + { + _templateApplied = false; + _templateRoot = null; - protected T? FindTemplateChild(string name) where T : SkiaView - { - if (_templateRoot == null) - { - return null; - } - return FindChild(_templateRoot, name); - } + if (newTemplate != null) + { + ApplyTemplate(); + } - private static T? FindChild(SkiaView root, string name) where T : SkiaView - { - if (root is T result && root.Name == name) - { - return result; - } - if (root is SkiaLayoutView skiaLayoutView) - { - foreach (SkiaView child in skiaLayoutView.Children) - { - T val = FindChild(child, name); - if (val != null) - { - return val; - } - } - } - else if (root is SkiaContentPresenter { Content: not null } skiaContentPresenter) - { - return FindChild(skiaContentPresenter.Content, name); - } - return null; - } + InvalidateMeasure(); + } - protected virtual SkiaView? ConvertElementToSkiaView(Element element) - { - //IL_008d: Unknown result type (might be due to invalid IL or missing references) - StackLayout val = (StackLayout)(object)((element is StackLayout) ? element : null); - if (val == null) - { - Grid val2 = (Grid)(object)((element is Grid) ? element : null); - if (val2 == null) - { - Border val3 = (Border)(object)((element is Border) ? element : null); - if (val3 == null) - { - Label val4 = (Label)(object)((element is Label) ? element : null); - if (val4 == null) - { - if (element is ContentPresenter) - { - return new SkiaContentPresenter(); - } - return new SkiaLabel - { - Text = "[" + ((object)element).GetType().Name + "]", - TextColor = SKColors.Gray - }; - } - return CreateSkiaLabel(val4); - } - return CreateSkiaBorder(val3); - } - return CreateSkiaGrid(val2); - } - return CreateSkiaStackLayout(val); - } + /// + /// Applies the current ControlTemplate if one is set. + /// + protected virtual void ApplyTemplate() + { + if (ControlTemplate == null || _templateApplied) + return; - private SkiaStackLayout CreateSkiaStackLayout(StackLayout sl) - { - //IL_0007: Unknown result type (might be due to invalid IL or missing references) - SkiaStackLayout skiaStackLayout = new SkiaStackLayout - { - Orientation = (((int)sl.Orientation != 0) ? StackOrientation.Horizontal : StackOrientation.Vertical), - Spacing = (float)((StackBase)sl).Spacing - }; - foreach (IView child in ((Layout)sl).Children) - { - Element val = (Element)(object)((child is Element) ? child : null); - if (val != null) - { - SkiaView skiaView = ConvertElementToSkiaView(val); - if (skiaView != null) - { - skiaStackLayout.AddChild(skiaView); - } - } - } - return skiaStackLayout; - } + try + { + // Create content from template + var content = ControlTemplate.CreateContent(); - private SkiaGrid CreateSkiaGrid(Grid grid) - { - //IL_001c: Unknown result type (might be due to invalid IL or missing references) - //IL_0021: Unknown result type (might be due to invalid IL or missing references) - //IL_002d: Unknown result type (might be due to invalid IL or missing references) - //IL_0032: Unknown result type (might be due to invalid IL or missing references) - //IL_0056: Unknown result type (might be due to invalid IL or missing references) - //IL_005b: Unknown result type (might be due to invalid IL or missing references) - //IL_003e: Unknown result type (might be due to invalid IL or missing references) - //IL_0043: Unknown result type (might be due to invalid IL or missing references) - //IL_00ad: Unknown result type (might be due to invalid IL or missing references) - //IL_00b2: Unknown result type (might be due to invalid IL or missing references) - //IL_00bf: Unknown result type (might be due to invalid IL or missing references) - //IL_00c4: Unknown result type (might be due to invalid IL or missing references) - //IL_00ea: Unknown result type (might be due to invalid IL or missing references) - //IL_00ef: Unknown result type (might be due to invalid IL or missing references) - //IL_00d1: Unknown result type (might be due to invalid IL or missing references) - //IL_00d6: Unknown result type (might be due to invalid IL or missing references) - //IL_0161: Unknown result type (might be due to invalid IL or missing references) - //IL_016b: Expected O, but got Unknown - //IL_016f: Unknown result type (might be due to invalid IL or missing references) - //IL_0179: Expected O, but got Unknown - //IL_017d: Unknown result type (might be due to invalid IL or missing references) - //IL_0187: Expected O, but got Unknown - //IL_018b: Unknown result type (might be due to invalid IL or missing references) - //IL_0195: Expected O, but got Unknown - SkiaGrid skiaGrid = new SkiaGrid(); - GridLength val; - foreach (RowDefinition item3 in (DefinitionCollection)(object)grid.RowDefinitions) - { - val = item3.Height; - GridLength gridLength; - if (!((GridLength)(ref val)).IsAuto) - { - val = item3.Height; - if (!((GridLength)(ref val)).IsStar) - { - val = item3.Height; - gridLength = new GridLength((float)((GridLength)(ref val)).Value); - } - else - { - val = item3.Height; - gridLength = new GridLength((float)((GridLength)(ref val)).Value, GridUnitType.Star); - } - } - else - { - gridLength = GridLength.Auto; - } - GridLength item = gridLength; - skiaGrid.RowDefinitions.Add(item); - } - foreach (ColumnDefinition item4 in (DefinitionCollection)(object)grid.ColumnDefinitions) - { - val = item4.Width; - GridLength gridLength2; - if (!((GridLength)(ref val)).IsAuto) - { - val = item4.Width; - if (!((GridLength)(ref val)).IsStar) - { - val = item4.Width; - gridLength2 = new GridLength((float)((GridLength)(ref val)).Value); - } - else - { - val = item4.Width; - gridLength2 = new GridLength((float)((GridLength)(ref val)).Value, GridUnitType.Star); - } - } - else - { - gridLength2 = GridLength.Auto; - } - GridLength item2 = gridLength2; - skiaGrid.ColumnDefinitions.Add(item2); - } - foreach (IView child in ((Layout)grid).Children) - { - Element val2 = (Element)(object)((child is Element) ? child : null); - if (val2 != null) - { - SkiaView skiaView = ConvertElementToSkiaView(val2); - if (skiaView != null) - { - int row = Grid.GetRow((BindableObject)child); - int column = Grid.GetColumn((BindableObject)child); - int rowSpan = Grid.GetRowSpan((BindableObject)child); - int columnSpan = Grid.GetColumnSpan((BindableObject)child); - skiaGrid.AddChild(skiaView, row, column, rowSpan, columnSpan); - } - } - } - return skiaGrid; - } + // If the content is a MAUI Element, try to convert it to a SkiaView + if (content is Element element) + { + _templateRoot = ConvertElementToSkiaView(element); + } + else if (content is SkiaView skiaView) + { + _templateRoot = skiaView; + } - private SkiaBorder CreateSkiaBorder(Border border) - { - //IL_0016: Unknown result type (might be due to invalid IL or missing references) - //IL_001b: Unknown result type (might be due to invalid IL or missing references) - //IL_0056: Unknown result type (might be due to invalid IL or missing references) - //IL_0079: Unknown result type (might be due to invalid IL or missing references) - float cornerRadius = 0f; - IShape strokeShape = border.StrokeShape; - RoundRectangle val = (RoundRectangle)(object)((strokeShape is RoundRectangle) ? strokeShape : null); - if (val != null) - { - CornerRadius cornerRadius2 = val.CornerRadius; - cornerRadius = (float)((CornerRadius)(ref cornerRadius2)).TopLeft; - } - SkiaBorder skiaBorder = new SkiaBorder - { - CornerRadius = cornerRadius, - StrokeThickness = (float)border.StrokeThickness - }; - Brush stroke = border.Stroke; - SolidColorBrush val2 = (SolidColorBrush)(object)((stroke is SolidColorBrush) ? stroke : null); - if (val2 != null) - { - skiaBorder.Stroke = val2.Color.ToSKColor(); - } - Brush background = ((VisualElement)border).Background; - SolidColorBrush val3 = (SolidColorBrush)(object)((background is SolidColorBrush) ? background : null); - if (val3 != null) - { - skiaBorder.BackgroundColor = val3.Color.ToSKColor(); - } - Element content = (Element)(object)border.Content; - if (content != null) - { - SkiaView skiaView = ConvertElementToSkiaView(content); - if (skiaView != null) - { - skiaBorder.AddChild(skiaView); - } - } - return skiaBorder; - } + if (_templateRoot != null) + { + _templateRoot.Parent = this; + OnTemplateApplied(); + } - private SkiaLabel CreateSkiaLabel(Label label) - { - //IL_0037: Unknown result type (might be due to invalid IL or missing references) - SkiaLabel skiaLabel = new SkiaLabel - { - Text = (label.Text ?? ""), - FontSize = (float)label.FontSize - }; - if (label.TextColor != null) - { - skiaLabel.TextColor = label.TextColor.ToSKColor(); - } - return skiaLabel; - } + _templateApplied = true; + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Error applying template: {ex.Message}"); + } + } - protected override void OnDraw(SKCanvas canvas, SKRect bounds) - { - //IL_001f: Unknown result type (might be due to invalid IL or missing references) - if (_templateRoot != null && _templateApplied) - { - _templateRoot.Draw(canvas); - } - else - { - DrawDefaultAppearance(canvas, bounds); - } - } + /// + /// Called after a template has been successfully applied. + /// Override to perform template-specific initialization. + /// + protected virtual void OnTemplateApplied() + { + // Find and bind ContentPresenter if present + var presenter = FindTemplateChild("PART_ContentPresenter"); + if (presenter != null) + { + OnContentPresenterFound(presenter); + } + } - protected abstract void DrawDefaultAppearance(SKCanvas canvas, SKRect bounds); + /// + /// Called when a ContentPresenter is found in the template. + /// Override to set up the content binding. + /// + protected virtual void OnContentPresenterFound(SkiaContentPresenter presenter) + { + // Derived classes should override to bind their content + } - protected override SKSize MeasureOverride(SKSize availableSize) - { - //IL_001e: Unknown result type (might be due to invalid IL or missing references) - //IL_001f: Unknown result type (might be due to invalid IL or missing references) - //IL_0016: Unknown result type (might be due to invalid IL or missing references) - //IL_0017: Unknown result type (might be due to invalid IL or missing references) - if (_templateRoot != null && _templateApplied) - { - return _templateRoot.Measure(availableSize); - } - return MeasureDefaultAppearance(availableSize); - } + /// + /// Finds a named element in the template tree. + /// + protected T? FindTemplateChild(string name) where T : SkiaView + { + if (_templateRoot == null) + return null; - protected virtual SKSize MeasureDefaultAppearance(SKSize availableSize) - { - //IL_000a: Unknown result type (might be due to invalid IL or missing references) - return new SKSize(100f, 40f); - } + return FindChild(_templateRoot, name); + } - public new void Arrange(SKRect bounds) - { - //IL_0001: Unknown result type (might be due to invalid IL or missing references) - //IL_001d: Unknown result type (might be due to invalid IL or missing references) - base.Arrange(bounds); - if (_templateRoot != null && _templateApplied) - { - _templateRoot.Arrange(bounds); - } - } + private static T? FindChild(SkiaView root, string name) where T : SkiaView + { + if (root is T typed && root.Name == name) + return typed; - public override SkiaView? HitTest(float x, float y) - { - //IL_0009: Unknown result type (might be due to invalid IL or missing references) - //IL_000e: Unknown result type (might be due to invalid IL or missing references) - if (base.IsVisible) - { - SKRect bounds = base.Bounds; - if (((SKRect)(ref bounds)).Contains(x, y)) - { - if (_templateRoot != null && _templateApplied) - { - SkiaView skiaView = _templateRoot.HitTest(x, y); - if (skiaView != null) - { - return skiaView; - } - } - return this; - } - } - return null; - } + if (root is SkiaLayoutView layout) + { + foreach (var child in layout.Children) + { + var found = FindChild(child, name); + if (found != null) + return found; + } + } + else if (root is SkiaContentPresenter presenter && presenter.Content != null) + { + return FindChild(presenter.Content, name); + } + + return null; + } + + /// + /// Converts a MAUI Element to a SkiaView. + /// Override to provide custom conversion logic. + /// + protected virtual SkiaView? ConvertElementToSkiaView(Element element) + { + // This is a simplified conversion - in a full implementation, + // you would use the handler system to create proper platform views + + return element switch + { + // Handle common layout types + Microsoft.Maui.Controls.StackLayout sl => CreateSkiaStackLayout(sl), + Microsoft.Maui.Controls.Grid grid => CreateSkiaGrid(grid), + Microsoft.Maui.Controls.Border border => CreateSkiaBorder(border), + Microsoft.Maui.Controls.Label label => CreateSkiaLabel(label), + Microsoft.Maui.Controls.ContentPresenter cp => new SkiaContentPresenter(), + _ => new SkiaLabel { Text = $"[{element.GetType().Name}]", TextColor = SKColors.Gray } + }; + } + + private SkiaStackLayout CreateSkiaStackLayout(Microsoft.Maui.Controls.StackLayout sl) + { + var layout = new SkiaStackLayout + { + Orientation = sl.Orientation == Microsoft.Maui.Controls.StackOrientation.Vertical + ? StackOrientation.Vertical + : StackOrientation.Horizontal, + Spacing = (float)sl.Spacing + }; + + foreach (var child in sl.Children) + { + if (child is Element element) + { + var skiaChild = ConvertElementToSkiaView(element); + if (skiaChild != null) + layout.AddChild(skiaChild); + } + } + + return layout; + } + + private SkiaGrid CreateSkiaGrid(Microsoft.Maui.Controls.Grid grid) + { + var layout = new SkiaGrid(); + + // Set row definitions + foreach (var rowDef in grid.RowDefinitions) + { + var gridLength = rowDef.Height.IsAuto ? GridLength.Auto : + rowDef.Height.IsStar ? new GridLength((float)rowDef.Height.Value, GridUnitType.Star) : + new GridLength((float)rowDef.Height.Value, GridUnitType.Absolute); + layout.RowDefinitions.Add(gridLength); + } + + // Set column definitions + foreach (var colDef in grid.ColumnDefinitions) + { + var gridLength = colDef.Width.IsAuto ? GridLength.Auto : + colDef.Width.IsStar ? new GridLength((float)colDef.Width.Value, GridUnitType.Star) : + new GridLength((float)colDef.Width.Value, GridUnitType.Absolute); + layout.ColumnDefinitions.Add(gridLength); + } + + // Add children + foreach (var child in grid.Children) + { + if (child is Element element) + { + var skiaChild = ConvertElementToSkiaView(element); + if (skiaChild != null) + { + var row = Microsoft.Maui.Controls.Grid.GetRow((BindableObject)child); + var col = Microsoft.Maui.Controls.Grid.GetColumn((BindableObject)child); + var rowSpan = Microsoft.Maui.Controls.Grid.GetRowSpan((BindableObject)child); + var colSpan = Microsoft.Maui.Controls.Grid.GetColumnSpan((BindableObject)child); + + layout.AddChild(skiaChild, row, col, rowSpan, colSpan); + } + } + } + + return layout; + } + + private SkiaBorder CreateSkiaBorder(Microsoft.Maui.Controls.Border border) + { + float cornerRadius = 0; + if (border.StrokeShape is Microsoft.Maui.Controls.Shapes.RoundRectangle rr) + { + cornerRadius = (float)rr.CornerRadius.TopLeft; + } + + var skiaBorder = new SkiaBorder + { + CornerRadius = cornerRadius, + StrokeThickness = (float)border.StrokeThickness + }; + + if (border.Stroke is SolidColorBrush strokeBrush) + { + skiaBorder.Stroke = strokeBrush.Color.ToSKColor(); + } + + if (border.Background is SolidColorBrush bgBrush) + { + skiaBorder.BackgroundColor = bgBrush.Color.ToSKColor(); + } + + if (border.Content is Element content) + { + var skiaContent = ConvertElementToSkiaView(content); + if (skiaContent != null) + skiaBorder.AddChild(skiaContent); + } + + return skiaBorder; + } + + private SkiaLabel CreateSkiaLabel(Microsoft.Maui.Controls.Label label) + { + var skiaLabel = new SkiaLabel + { + Text = label.Text ?? "", + FontSize = (float)label.FontSize + }; + + if (label.TextColor != null) + { + skiaLabel.TextColor = label.TextColor.ToSKColor(); + } + + return skiaLabel; + } + + protected override void OnDraw(SKCanvas canvas, SKRect bounds) + { + if (_templateRoot != null && _templateApplied) + { + // Render the template + _templateRoot.Draw(canvas); + } + else + { + // Render default appearance + DrawDefaultAppearance(canvas, bounds); + } + } + + /// + /// Draws the default appearance when no template is applied. + /// Override in derived classes to provide default rendering. + /// + protected abstract void DrawDefaultAppearance(SKCanvas canvas, SKRect bounds); + + protected override SKSize MeasureOverride(SKSize availableSize) + { + if (_templateRoot != null && _templateApplied) + { + return _templateRoot.Measure(availableSize); + } + + return MeasureDefaultAppearance(availableSize); + } + + /// + /// Measures the default appearance when no template is applied. + /// Override in derived classes. + /// + protected virtual SKSize MeasureDefaultAppearance(SKSize availableSize) + { + return new SKSize(100, 40); + } + + public new void Arrange(SKRect bounds) + { + base.Arrange(bounds); + + if (_templateRoot != null && _templateApplied) + { + _templateRoot.Arrange(bounds); + } + } + + public override SkiaView? HitTest(float x, float y) + { + if (!IsVisible || !Bounds.Contains(x, y)) + return null; + + if (_templateRoot != null && _templateApplied) + { + var hit = _templateRoot.HitTest(x, y); + if (hit != null) + return hit; + } + + return this; + } } + diff --git a/Views/SkiaTextAlignment.cs b/Views/SkiaTextAlignment.cs deleted file mode 100644 index 60ac31a..0000000 --- a/Views/SkiaTextAlignment.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Microsoft.Maui.Platform; - -public enum SkiaTextAlignment -{ - Left, - Center, - Right -} diff --git a/Views/SkiaTextSpan.cs b/Views/SkiaTextSpan.cs deleted file mode 100644 index c592bdd..0000000 --- a/Views/SkiaTextSpan.cs +++ /dev/null @@ -1,28 +0,0 @@ -using SkiaSharp; - -namespace Microsoft.Maui.Platform; - -public class SkiaTextSpan -{ - public string Text { get; set; } = ""; - - public SKColor? TextColor { get; set; } - - public SKColor? BackgroundColor { get; set; } - - public string? FontFamily { get; set; } - - public float FontSize { get; set; } - - public bool IsBold { get; set; } - - public bool IsItalic { get; set; } - - public bool IsUnderline { get; set; } - - public bool IsStrikethrough { get; set; } - - public float CharacterSpacing { get; set; } - - public float LineHeight { get; set; } = 1f; -} diff --git a/Views/SkiaTimePicker.cs b/Views/SkiaTimePicker.cs index 3c629bd..3534a60 100644 --- a/Views/SkiaTimePicker.cs +++ b/Views/SkiaTimePicker.cs @@ -1,953 +1,489 @@ -using System; -using Microsoft.Maui.Controls; -using Microsoft.Maui.Platform.Linux; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using SkiaSharp; +using Microsoft.Maui.Platform.Linux; namespace Microsoft.Maui.Platform; +/// +/// Skia-rendered time picker control with clock popup. +/// public class SkiaTimePicker : SkiaView { - public static readonly BindableProperty TimeProperty = BindableProperty.Create("Time", typeof(TimeSpan), typeof(SkiaTimePicker), (object)DateTime.Now.TimeOfDay, (BindingMode)1, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaTimePicker)(object)b).OnTimePropertyChanged(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + #region BindableProperties - public static readonly BindableProperty FormatProperty = BindableProperty.Create("Format", typeof(string), typeof(SkiaTimePicker), (object)"t", (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaTimePicker)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + public static readonly BindableProperty TimeProperty = + BindableProperty.Create(nameof(Time), typeof(TimeSpan), typeof(SkiaTimePicker), DateTime.Now.TimeOfDay, BindingMode.TwoWay, + propertyChanged: (b, o, n) => ((SkiaTimePicker)b).OnTimePropertyChanged()); - public static readonly BindableProperty TextColorProperty = BindableProperty.Create("TextColor", typeof(SKColor), typeof(SkiaTimePicker), (object)SKColors.Black, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaTimePicker)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + public static readonly BindableProperty FormatProperty = + BindableProperty.Create(nameof(Format), typeof(string), typeof(SkiaTimePicker), "t", + propertyChanged: (b, o, n) => ((SkiaTimePicker)b).Invalidate()); - public static readonly BindableProperty BorderColorProperty = BindableProperty.Create("BorderColor", typeof(SKColor), typeof(SkiaTimePicker), (object)new SKColor((byte)189, (byte)189, (byte)189), (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaTimePicker)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + public static readonly BindableProperty TextColorProperty = + BindableProperty.Create(nameof(TextColor), typeof(SKColor), typeof(SkiaTimePicker), SKColors.Black, + propertyChanged: (b, o, n) => ((SkiaTimePicker)b).Invalidate()); - public static readonly BindableProperty ClockBackgroundColorProperty = BindableProperty.Create("ClockBackgroundColor", typeof(SKColor), typeof(SkiaTimePicker), (object)SKColors.White, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaTimePicker)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + public static readonly BindableProperty BorderColorProperty = + BindableProperty.Create(nameof(BorderColor), typeof(SKColor), typeof(SkiaTimePicker), new SKColor(0xBD, 0xBD, 0xBD), + propertyChanged: (b, o, n) => ((SkiaTimePicker)b).Invalidate()); - public static readonly BindableProperty ClockFaceColorProperty = BindableProperty.Create("ClockFaceColor", typeof(SKColor), typeof(SkiaTimePicker), (object)new SKColor((byte)245, (byte)245, (byte)245), (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaTimePicker)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + public static readonly BindableProperty ClockBackgroundColorProperty = + BindableProperty.Create(nameof(ClockBackgroundColor), typeof(SKColor), typeof(SkiaTimePicker), SKColors.White, + propertyChanged: (b, o, n) => ((SkiaTimePicker)b).Invalidate()); - public static readonly BindableProperty SelectedColorProperty = BindableProperty.Create("SelectedColor", typeof(SKColor), typeof(SkiaTimePicker), (object)new SKColor((byte)33, (byte)150, (byte)243), (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaTimePicker)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + public static readonly BindableProperty ClockFaceColorProperty = + BindableProperty.Create(nameof(ClockFaceColor), typeof(SKColor), typeof(SkiaTimePicker), new SKColor(0xF5, 0xF5, 0xF5), + propertyChanged: (b, o, n) => ((SkiaTimePicker)b).Invalidate()); - public static readonly BindableProperty HeaderColorProperty = BindableProperty.Create("HeaderColor", typeof(SKColor), typeof(SkiaTimePicker), (object)new SKColor((byte)33, (byte)150, (byte)243), (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaTimePicker)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + public static readonly BindableProperty SelectedColorProperty = + BindableProperty.Create(nameof(SelectedColor), typeof(SKColor), typeof(SkiaTimePicker), new SKColor(0x21, 0x96, 0xF3), + propertyChanged: (b, o, n) => ((SkiaTimePicker)b).Invalidate()); - public static readonly BindableProperty FontSizeProperty = BindableProperty.Create("FontSize", typeof(float), typeof(SkiaTimePicker), (object)14f, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaTimePicker)(object)b).InvalidateMeasure(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + public static readonly BindableProperty HeaderColorProperty = + BindableProperty.Create(nameof(HeaderColor), typeof(SKColor), typeof(SkiaTimePicker), new SKColor(0x21, 0x96, 0xF3), + propertyChanged: (b, o, n) => ((SkiaTimePicker)b).Invalidate()); - public static readonly BindableProperty CornerRadiusProperty = BindableProperty.Create("CornerRadius", typeof(float), typeof(SkiaTimePicker), (object)4f, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaTimePicker)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + public static readonly BindableProperty FontSizeProperty = + BindableProperty.Create(nameof(FontSize), typeof(float), typeof(SkiaTimePicker), 14f, + propertyChanged: (b, o, n) => ((SkiaTimePicker)b).InvalidateMeasure()); - private bool _isOpen; + public static readonly BindableProperty CornerRadiusProperty = + BindableProperty.Create(nameof(CornerRadius), typeof(float), typeof(SkiaTimePicker), 4f, + propertyChanged: (b, o, n) => ((SkiaTimePicker)b).Invalidate()); - private int _selectedHour; + #endregion - private int _selectedMinute; + #region Properties - private bool _isSelectingHours = true; + public TimeSpan Time + { + get => (TimeSpan)GetValue(TimeProperty); + set => SetValue(TimeProperty, value); + } - private const float ClockSize = 280f; + public string Format + { + get => (string)GetValue(FormatProperty); + set => SetValue(FormatProperty, value); + } - private const float ClockRadius = 100f; + public SKColor TextColor + { + get => (SKColor)GetValue(TextColorProperty); + set => SetValue(TextColorProperty, value); + } - private const float HeaderHeight = 80f; + public SKColor BorderColor + { + get => (SKColor)GetValue(BorderColorProperty); + set => SetValue(BorderColorProperty, value); + } - private const float PopupHeight = 360f; + public SKColor ClockBackgroundColor + { + get => (SKColor)GetValue(ClockBackgroundColorProperty); + set => SetValue(ClockBackgroundColorProperty, value); + } - public TimeSpan Time - { - get - { - return (TimeSpan)((BindableObject)this).GetValue(TimeProperty); - } - set - { - ((BindableObject)this).SetValue(TimeProperty, (object)value); - } - } + public SKColor ClockFaceColor + { + get => (SKColor)GetValue(ClockFaceColorProperty); + set => SetValue(ClockFaceColorProperty, value); + } - public string Format - { - get - { - return (string)((BindableObject)this).GetValue(FormatProperty); - } - set - { - ((BindableObject)this).SetValue(FormatProperty, (object)value); - } - } + public SKColor SelectedColor + { + get => (SKColor)GetValue(SelectedColorProperty); + set => SetValue(SelectedColorProperty, value); + } - public SKColor TextColor - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKColor)((BindableObject)this).GetValue(TextColorProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(TextColorProperty, (object)value); - } - } + public SKColor HeaderColor + { + get => (SKColor)GetValue(HeaderColorProperty); + set => SetValue(HeaderColorProperty, value); + } - public SKColor BorderColor - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKColor)((BindableObject)this).GetValue(BorderColorProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(BorderColorProperty, (object)value); - } - } + public float FontSize + { + get => (float)GetValue(FontSizeProperty); + set => SetValue(FontSizeProperty, value); + } - public SKColor ClockBackgroundColor - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKColor)((BindableObject)this).GetValue(ClockBackgroundColorProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(ClockBackgroundColorProperty, (object)value); - } - } + public float CornerRadius + { + get => (float)GetValue(CornerRadiusProperty); + set => SetValue(CornerRadiusProperty, value); + } - public SKColor ClockFaceColor - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKColor)((BindableObject)this).GetValue(ClockFaceColorProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(ClockFaceColorProperty, (object)value); - } - } + public bool IsOpen + { + get => _isOpen; + set + { + if (_isOpen != value) + { + _isOpen = value; + if (_isOpen) + RegisterPopupOverlay(this, DrawClockOverlay); + else + UnregisterPopupOverlay(this); + Invalidate(); + } + } + } - public SKColor SelectedColor - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKColor)((BindableObject)this).GetValue(SelectedColorProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(SelectedColorProperty, (object)value); - } - } + #endregion - public SKColor HeaderColor - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (SKColor)((BindableObject)this).GetValue(HeaderColorProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(HeaderColorProperty, (object)value); - } - } + private bool _isOpen; + private int _selectedHour; + private int _selectedMinute; + private bool _isSelectingHours = true; - public float FontSize - { - get - { - return (float)((BindableObject)this).GetValue(FontSizeProperty); - } - set - { - ((BindableObject)this).SetValue(FontSizeProperty, (object)value); - } - } + private const float ClockSize = 280; + private const float ClockRadius = 100; + private const float HeaderHeight = 80; + private const float PopupHeight = ClockSize + HeaderHeight; - public float CornerRadius - { - get - { - return (float)((BindableObject)this).GetValue(CornerRadiusProperty); - } - set - { - ((BindableObject)this).SetValue(CornerRadiusProperty, (object)value); - } - } + public event EventHandler? TimeSelected; - public bool IsOpen - { - get - { - return _isOpen; - } - set - { - if (_isOpen != value) - { - _isOpen = value; - if (_isOpen) - { - SkiaView.RegisterPopupOverlay(this, DrawClockOverlay); - } - else - { - SkiaView.UnregisterPopupOverlay(this); - } - Invalidate(); - } - } - } + /// + /// Gets the clock popup rectangle with edge detection applied. + /// + private SKRect GetPopupRect(SKRect pickerBounds) + { + // Get window dimensions for edge detection + var windowWidth = LinuxApplication.Current?.MainWindow?.Width ?? 800; + var windowHeight = LinuxApplication.Current?.MainWindow?.Height ?? 600; - public event EventHandler? TimeSelected; + // Calculate default position (below the picker) + var popupLeft = pickerBounds.Left; + var popupTop = pickerBounds.Bottom + 4; - private SKRect GetPopupRect(SKRect pickerBounds) - { - //IL_0101: Unknown result type (might be due to invalid IL or missing references) - int num = LinuxApplication.Current?.MainWindow?.Width ?? 800; - int num2 = LinuxApplication.Current?.MainWindow?.Height ?? 600; - float num3 = ((SKRect)(ref pickerBounds)).Left; - float num4 = ((SKRect)(ref pickerBounds)).Bottom + 4f; - if (num3 + 280f > (float)num) - { - num3 = (float)num - 280f - 4f; - } - if (num3 < 0f) - { - num3 = 4f; - } - if (num4 + 360f > (float)num2) - { - num4 = ((SKRect)(ref pickerBounds)).Top - 360f - 4f; - } - if (num4 < 0f) - { - num4 = 4f; - } - return new SKRect(num3, num4, num3 + 280f, num4 + 360f); - } + // Edge detection: adjust horizontal position if popup would go off-screen + if (popupLeft + ClockSize > windowWidth) + { + popupLeft = windowWidth - ClockSize - 4; + } + if (popupLeft < 0) popupLeft = 4; - public SkiaTimePicker() - { - base.IsFocusable = true; - _selectedHour = DateTime.Now.Hour; - _selectedMinute = DateTime.Now.Minute; - } + // Edge detection: show above if popup would go off-screen vertically + if (popupTop + PopupHeight > windowHeight) + { + popupTop = pickerBounds.Top - PopupHeight - 4; + } + if (popupTop < 0) popupTop = 4; - private void OnTimePropertyChanged() - { - _selectedHour = Time.Hours; - _selectedMinute = Time.Minutes; - this.TimeSelected?.Invoke(this, EventArgs.Empty); - Invalidate(); - } + return new SKRect(popupLeft, popupTop, popupLeft + ClockSize, popupTop + PopupHeight); + } - private void DrawClockOverlay(SKCanvas canvas) - { - //IL_000c: Unknown result type (might be due to invalid IL or missing references) - if (_isOpen) - { - DrawClockPopup(canvas, base.ScreenBounds); - } - } + public SkiaTimePicker() + { + IsFocusable = true; + _selectedHour = DateTime.Now.Hour; + _selectedMinute = DateTime.Now.Minute; + } - protected override void OnDraw(SKCanvas canvas, SKRect bounds) - { - //IL_0002: Unknown result type (might be due to invalid IL or missing references) - DrawPickerButton(canvas, bounds); - } + private void OnTimePropertyChanged() + { + _selectedHour = Time.Hours; + _selectedMinute = Time.Minutes; + TimeSelected?.Invoke(this, EventArgs.Empty); + Invalidate(); + } - private void DrawPickerButton(SKCanvas canvas, SKRect bounds) - { - //IL_0000: Unknown result type (might be due to invalid IL or missing references) - //IL_0005: Unknown result type (might be due to invalid IL or missing references) - //IL_0025: Unknown result type (might be due to invalid IL or missing references) - //IL_001d: Unknown result type (might be due to invalid IL or missing references) - //IL_002f: Unknown result type (might be due to invalid IL or missing references) - //IL_0036: Unknown result type (might be due to invalid IL or missing references) - //IL_003e: Expected O, but got Unknown - //IL_003f: Unknown result type (might be due to invalid IL or missing references) - //IL_0046: Unknown result type (might be due to invalid IL or missing references) - //IL_0051: Expected O, but got Unknown - //IL_0051: Unknown result type (might be due to invalid IL or missing references) - //IL_0056: Unknown result type (might be due to invalid IL or missing references) - //IL_0068: Unknown result type (might be due to invalid IL or missing references) - //IL_0060: Unknown result type (might be due to invalid IL or missing references) - //IL_0072: Unknown result type (might be due to invalid IL or missing references) - //IL_0079: Unknown result type (might be due to invalid IL or missing references) - //IL_008c: Unknown result type (might be due to invalid IL or missing references) - //IL_0094: Expected O, but got Unknown - //IL_0095: Unknown result type (might be due to invalid IL or missing references) - //IL_009c: Unknown result type (might be due to invalid IL or missing references) - //IL_00a7: Expected O, but got Unknown - //IL_00bc: Unknown result type (might be due to invalid IL or missing references) - //IL_00c2: Expected O, but got Unknown - //IL_00c3: Unknown result type (might be due to invalid IL or missing references) - //IL_00c8: Unknown result type (might be due to invalid IL or missing references) - //IL_00e8: Unknown result type (might be due to invalid IL or missing references) - //IL_00d2: Unknown result type (might be due to invalid IL or missing references) - //IL_00d7: Unknown result type (might be due to invalid IL or missing references) - //IL_00e0: Unknown result type (might be due to invalid IL or missing references) - //IL_00f2: Unknown result type (might be due to invalid IL or missing references) - //IL_00fa: Expected O, but got Unknown - //IL_0121: Unknown result type (might be due to invalid IL or missing references) - //IL_018d: Unknown result type (might be due to invalid IL or missing references) - SKPaint val = new SKPaint - { - Color = (SKColor)(base.IsEnabled ? base.BackgroundColor : new SKColor((byte)245, (byte)245, (byte)245)), - Style = (SKPaintStyle)0, - IsAntialias = true - }; - try - { - canvas.DrawRoundRect(new SKRoundRect(bounds, CornerRadius), val); - SKPaint val2 = new SKPaint - { - Color = (base.IsFocused ? SelectedColor : BorderColor), - Style = (SKPaintStyle)1, - StrokeWidth = ((!base.IsFocused) ? 1 : 2), - IsAntialias = true - }; - try - { - canvas.DrawRoundRect(new SKRoundRect(bounds, CornerRadius), val2); - SKFont val3 = new SKFont(SKTypeface.Default, FontSize, 1f, 0f); - try - { - SKPaint val4 = new SKPaint(val3); - SKColor color; - if (!base.IsEnabled) - { - SKColor textColor = TextColor; - color = ((SKColor)(ref textColor)).WithAlpha((byte)128); - } - else - { - color = TextColor; - } - val4.Color = color; - val4.IsAntialias = true; - SKPaint val5 = val4; - try - { - string text = DateTime.Today.Add(Time).ToString(Format); - SKRect val6 = default(SKRect); - val5.MeasureText(text, ref val6); - canvas.DrawText(text, ((SKRect)(ref bounds)).Left + 12f, ((SKRect)(ref bounds)).MidY - ((SKRect)(ref val6)).MidY, val5); - DrawClockIcon(canvas, new SKRect(((SKRect)(ref bounds)).Right - 36f, ((SKRect)(ref bounds)).MidY - 10f, ((SKRect)(ref bounds)).Right - 12f, ((SKRect)(ref bounds)).MidY + 10f)); - } - finally - { - ((IDisposable)val5)?.Dispose(); - } - } - finally - { - ((IDisposable)val3)?.Dispose(); - } - } - finally - { - ((IDisposable)val2)?.Dispose(); - } - } - finally - { - ((IDisposable)val)?.Dispose(); - } - } + private void DrawClockOverlay(SKCanvas canvas) + { + if (!_isOpen) return; + // Use ScreenBounds for popup drawing (accounts for scroll offset) + DrawClockPopup(canvas, ScreenBounds); + } - private void DrawClockIcon(SKCanvas canvas, SKRect bounds) - { - //IL_0000: Unknown result type (might be due to invalid IL or missing references) - //IL_0005: Unknown result type (might be due to invalid IL or missing references) - //IL_0024: Unknown result type (might be due to invalid IL or missing references) - //IL_000f: Unknown result type (might be due to invalid IL or missing references) - //IL_0014: Unknown result type (might be due to invalid IL or missing references) - //IL_001c: Unknown result type (might be due to invalid IL or missing references) - //IL_002e: Unknown result type (might be due to invalid IL or missing references) - //IL_0035: Unknown result type (might be due to invalid IL or missing references) - //IL_0040: Unknown result type (might be due to invalid IL or missing references) - //IL_0048: Expected O, but got Unknown - SKPaint val = new SKPaint(); - SKColor color; - if (!base.IsEnabled) - { - SKColor textColor = TextColor; - color = ((SKColor)(ref textColor)).WithAlpha((byte)128); - } - else - { - color = TextColor; - } - val.Color = color; - val.Style = (SKPaintStyle)1; - val.StrokeWidth = 1.5f; - val.IsAntialias = true; - SKPaint val2 = val; - try - { - float num = Math.Min(((SKRect)(ref bounds)).Width, ((SKRect)(ref bounds)).Height) / 2f - 2f; - canvas.DrawCircle(((SKRect)(ref bounds)).MidX, ((SKRect)(ref bounds)).MidY, num, val2); - canvas.DrawLine(((SKRect)(ref bounds)).MidX, ((SKRect)(ref bounds)).MidY, ((SKRect)(ref bounds)).MidX, ((SKRect)(ref bounds)).MidY - num * 0.5f, val2); - canvas.DrawLine(((SKRect)(ref bounds)).MidX, ((SKRect)(ref bounds)).MidY, ((SKRect)(ref bounds)).MidX + num * 0.4f, ((SKRect)(ref bounds)).MidY, val2); - val2.Style = (SKPaintStyle)0; - canvas.DrawCircle(((SKRect)(ref bounds)).MidX, ((SKRect)(ref bounds)).MidY, 1.5f, val2); - } - finally - { - ((IDisposable)val2)?.Dispose(); - } - } + protected override void OnDraw(SKCanvas canvas, SKRect bounds) + { + DrawPickerButton(canvas, bounds); + } - private void DrawClockPopup(SKCanvas canvas, SKRect bounds) - { - //IL_0001: Unknown result type (might be due to invalid IL or missing references) - //IL_0002: Unknown result type (might be due to invalid IL or missing references) - //IL_0007: Unknown result type (might be due to invalid IL or missing references) - //IL_0008: Unknown result type (might be due to invalid IL or missing references) - //IL_000d: Unknown result type (might be due to invalid IL or missing references) - //IL_0013: Unknown result type (might be due to invalid IL or missing references) - //IL_001d: Unknown result type (might be due to invalid IL or missing references) - //IL_002e: Unknown result type (might be due to invalid IL or missing references) - //IL_0036: Expected O, but got Unknown - //IL_006b: Unknown result type (might be due to invalid IL or missing references) - //IL_0076: Unknown result type (might be due to invalid IL or missing references) - //IL_0081: Expected O, but got Unknown - //IL_0081: Unknown result type (might be due to invalid IL or missing references) - //IL_0086: Unknown result type (might be due to invalid IL or missing references) - //IL_0088: Unknown result type (might be due to invalid IL or missing references) - //IL_0092: Unknown result type (might be due to invalid IL or missing references) - //IL_0099: Unknown result type (might be due to invalid IL or missing references) - //IL_00a1: Expected O, but got Unknown - //IL_00a2: Unknown result type (might be due to invalid IL or missing references) - //IL_00a9: Unknown result type (might be due to invalid IL or missing references) - //IL_00b4: Expected O, but got Unknown - //IL_00b4: Unknown result type (might be due to invalid IL or missing references) - //IL_00b9: Unknown result type (might be due to invalid IL or missing references) - //IL_00bb: Unknown result type (might be due to invalid IL or missing references) - //IL_00c5: Unknown result type (might be due to invalid IL or missing references) - //IL_00cc: Unknown result type (might be due to invalid IL or missing references) - //IL_00d7: Unknown result type (might be due to invalid IL or missing references) - //IL_00df: Expected O, but got Unknown - //IL_00e0: Unknown result type (might be due to invalid IL or missing references) - //IL_00e7: Unknown result type (might be due to invalid IL or missing references) - //IL_00f2: Expected O, but got Unknown - //IL_0116: Unknown result type (might be due to invalid IL or missing references) - //IL_0144: Unknown result type (might be due to invalid IL or missing references) - SKRect popupRect = GetPopupRect(bounds); - SKPaint val = new SKPaint - { - Color = new SKColor((byte)0, (byte)0, (byte)0, (byte)40), - MaskFilter = SKMaskFilter.CreateBlur((SKBlurStyle)0, 4f), - Style = (SKPaintStyle)0 - }; - try - { - canvas.DrawRoundRect(new SKRoundRect(new SKRect(((SKRect)(ref popupRect)).Left + 2f, ((SKRect)(ref popupRect)).Top + 2f, ((SKRect)(ref popupRect)).Right + 2f, ((SKRect)(ref popupRect)).Bottom + 2f), CornerRadius), val); - SKPaint val2 = new SKPaint - { - Color = ClockBackgroundColor, - Style = (SKPaintStyle)0, - IsAntialias = true - }; - try - { - canvas.DrawRoundRect(new SKRoundRect(popupRect, CornerRadius), val2); - SKPaint val3 = new SKPaint - { - Color = BorderColor, - Style = (SKPaintStyle)1, - StrokeWidth = 1f, - IsAntialias = true - }; - try - { - canvas.DrawRoundRect(new SKRoundRect(popupRect, CornerRadius), val3); - DrawTimeHeader(canvas, new SKRect(((SKRect)(ref popupRect)).Left, ((SKRect)(ref popupRect)).Top, ((SKRect)(ref popupRect)).Right, ((SKRect)(ref popupRect)).Top + 80f)); - DrawClockFace(canvas, new SKRect(((SKRect)(ref popupRect)).Left, ((SKRect)(ref popupRect)).Top + 80f, ((SKRect)(ref popupRect)).Right, ((SKRect)(ref popupRect)).Bottom)); - } - finally - { - ((IDisposable)val3)?.Dispose(); - } - } - finally - { - ((IDisposable)val2)?.Dispose(); - } - } - finally - { - ((IDisposable)val)?.Dispose(); - } - } + private void DrawPickerButton(SKCanvas canvas, SKRect bounds) + { + using var bgPaint = new SKPaint + { + Color = IsEnabled ? BackgroundColor : new SKColor(0xF5, 0xF5, 0xF5), + Style = SKPaintStyle.Fill, + IsAntialias = true + }; + canvas.DrawRoundRect(new SKRoundRect(bounds, CornerRadius), bgPaint); - private void DrawTimeHeader(SKCanvas canvas, SKRect bounds) - { - //IL_0000: Unknown result type (might be due to invalid IL or missing references) - //IL_0005: Unknown result type (might be due to invalid IL or missing references) - //IL_0007: Unknown result type (might be due to invalid IL or missing references) - //IL_0011: Unknown result type (might be due to invalid IL or missing references) - //IL_0019: Expected O, but got Unknown - //IL_004a: Unknown result type (might be due to invalid IL or missing references) - //IL_0055: Unknown result type (might be due to invalid IL or missing references) - //IL_0061: Expected O, but got Unknown - //IL_0062: Unknown result type (might be due to invalid IL or missing references) - //IL_0093: Unknown result type (might be due to invalid IL or missing references) - //IL_00b2: Unknown result type (might be due to invalid IL or missing references) - //IL_00b8: Expected O, but got Unknown - //IL_00b9: Unknown result type (might be due to invalid IL or missing references) - //IL_00be: Unknown result type (might be due to invalid IL or missing references) - //IL_00bf: Unknown result type (might be due to invalid IL or missing references) - //IL_00c9: Unknown result type (might be due to invalid IL or missing references) - //IL_00d1: Expected O, but got Unknown - //IL_00d2: Unknown result type (might be due to invalid IL or missing references) - //IL_00d7: Unknown result type (might be due to invalid IL or missing references) - //IL_00ec: Unknown result type (might be due to invalid IL or missing references) - //IL_00f6: Unknown result type (might be due to invalid IL or missing references) - //IL_00fe: Expected O, but got Unknown - //IL_0140: Unknown result type (might be due to invalid IL or missing references) - //IL_0148: Unknown result type (might be due to invalid IL or missing references) - //IL_0150: Unknown result type (might be due to invalid IL or missing references) - SKPaint val = new SKPaint - { - Color = HeaderColor, - Style = (SKPaintStyle)0 - }; - try - { - canvas.Save(); - canvas.ClipRoundRect(new SKRoundRect(new SKRect(((SKRect)(ref bounds)).Left, ((SKRect)(ref bounds)).Top, ((SKRect)(ref bounds)).Right, ((SKRect)(ref bounds)).Top + CornerRadius * 2f), CornerRadius), (SKClipOperation)1, false); - canvas.DrawRect(bounds, val); - canvas.Restore(); - canvas.DrawRect(new SKRect(((SKRect)(ref bounds)).Left, ((SKRect)(ref bounds)).Top + CornerRadius, ((SKRect)(ref bounds)).Right, ((SKRect)(ref bounds)).Bottom), val); - SKFont val2 = new SKFont(SKTypeface.Default, 32f, 1f, 0f); - try - { - SKPaint val3 = new SKPaint(val2) - { - Color = SKColors.White, - IsAntialias = true - }; - try - { - SKPaint val4 = new SKPaint(val2) - { - Color = new SKColor(byte.MaxValue, byte.MaxValue, byte.MaxValue, (byte)150), - IsAntialias = true - }; - try - { - string text = _selectedHour.ToString("D2"); - string text2 = _selectedMinute.ToString("D2"); - SKPaint val5 = (_isSelectingHours ? val3 : val4); - SKPaint val6 = (_isSelectingHours ? val4 : val3); - SKRect val7 = default(SKRect); - SKRect val8 = default(SKRect); - SKRect val9 = default(SKRect); - val5.MeasureText(text, ref val7); - val3.MeasureText(":", ref val8); - val6.MeasureText(text2, ref val9); - float num = ((SKRect)(ref val7)).Width + ((SKRect)(ref val8)).Width + ((SKRect)(ref val9)).Width + 8f; - float num2 = ((SKRect)(ref bounds)).MidX - num / 2f; - float num3 = ((SKRect)(ref bounds)).MidY - ((SKRect)(ref val7)).MidY; - canvas.DrawText(text, num2, num3, val5); - canvas.DrawText(":", num2 + ((SKRect)(ref val7)).Width + 4f, num3, val3); - canvas.DrawText(text2, num2 + ((SKRect)(ref val7)).Width + ((SKRect)(ref val8)).Width + 8f, num3, val6); - } - finally - { - ((IDisposable)val4)?.Dispose(); - } - } - finally - { - ((IDisposable)val3)?.Dispose(); - } - } - finally - { - ((IDisposable)val2)?.Dispose(); - } - } - finally - { - ((IDisposable)val)?.Dispose(); - } - } + using var borderPaint = new SKPaint + { + Color = IsFocused ? SelectedColor : BorderColor, + Style = SKPaintStyle.Stroke, + StrokeWidth = IsFocused ? 2 : 1, + IsAntialias = true + }; + canvas.DrawRoundRect(new SKRoundRect(bounds, CornerRadius), borderPaint); - private void DrawClockFace(SKCanvas canvas, SKRect bounds) - { - //IL_0010: Unknown result type (might be due to invalid IL or missing references) - //IL_0015: Unknown result type (might be due to invalid IL or missing references) - //IL_0017: Unknown result type (might be due to invalid IL or missing references) - //IL_0021: Unknown result type (might be due to invalid IL or missing references) - //IL_0028: Unknown result type (might be due to invalid IL or missing references) - //IL_0030: Expected O, but got Unknown - //IL_0052: Unknown result type (might be due to invalid IL or missing references) - //IL_0058: Expected O, but got Unknown - //IL_0059: Unknown result type (might be due to invalid IL or missing references) - //IL_005e: Unknown result type (might be due to invalid IL or missing references) - //IL_0060: Unknown result type (might be due to invalid IL or missing references) - //IL_006a: Unknown result type (might be due to invalid IL or missing references) - //IL_0073: Expected O, but got Unknown - //IL_025f: Unknown result type (might be due to invalid IL or missing references) - //IL_0210: Unknown result type (might be due to invalid IL or missing references) - //IL_0215: Unknown result type (might be due to invalid IL or missing references) - //IL_0217: Unknown result type (might be due to invalid IL or missing references) - //IL_0221: Unknown result type (might be due to invalid IL or missing references) - //IL_0228: Unknown result type (might be due to invalid IL or missing references) - //IL_0231: Expected O, but got Unknown - //IL_0132: Unknown result type (might be due to invalid IL or missing references) - //IL_00e3: Unknown result type (might be due to invalid IL or missing references) - //IL_00e8: Unknown result type (might be due to invalid IL or missing references) - //IL_00ea: Unknown result type (might be due to invalid IL or missing references) - //IL_00f4: Unknown result type (might be due to invalid IL or missing references) - //IL_00fb: Unknown result type (might be due to invalid IL or missing references) - //IL_0104: Expected O, but got Unknown - //IL_026b: Unknown result type (might be due to invalid IL or missing references) - //IL_0244: Unknown result type (might be due to invalid IL or missing references) - //IL_013e: Unknown result type (might be due to invalid IL or missing references) - //IL_0117: Unknown result type (might be due to invalid IL or missing references) - float midX = ((SKRect)(ref bounds)).MidX; - float midY = ((SKRect)(ref bounds)).MidY; - SKPaint val = new SKPaint - { - Color = ClockFaceColor, - Style = (SKPaintStyle)0, - IsAntialias = true - }; - try - { - canvas.DrawCircle(midX, midY, 120f, val); - SKFont val2 = new SKFont(SKTypeface.Default, 14f, 1f, 0f); - try - { - SKPaint val3 = new SKPaint(val2) - { - Color = TextColor, - IsAntialias = true - }; - try - { - if (_isSelectingHours) - { - for (int i = 1; i <= 12; i++) - { - double num = (double)(i * 30 - 90) * Math.PI / 180.0; - float num2 = midX + (float)(100.0 * Math.Cos(num)); - float num3 = midY + (float)(100.0 * Math.Sin(num)); - if (_selectedHour % 12 == i % 12) - { - SKPaint val4 = new SKPaint - { - Color = SelectedColor, - Style = (SKPaintStyle)0, - IsAntialias = true - }; - try - { - canvas.DrawCircle(num2, num3, 18f, val4); - val3.Color = SKColors.White; - } - finally - { - ((IDisposable)val4)?.Dispose(); - } - } - else - { - val3.Color = TextColor; - } - SKRect val5 = default(SKRect); - val3.MeasureText(i.ToString(), ref val5); - canvas.DrawText(i.ToString(), num2 - ((SKRect)(ref val5)).MidX, num3 - ((SKRect)(ref val5)).MidY, val3); - } - DrawClockHand(canvas, midX, midY, _selectedHour % 12 * 30 - 90, 82f); - return; - } - for (int j = 0; j < 12; j++) - { - int num4 = j * 5; - double num5 = (double)(num4 * 6 - 90) * Math.PI / 180.0; - float num6 = midX + (float)(100.0 * Math.Cos(num5)); - float num7 = midY + (float)(100.0 * Math.Sin(num5)); - if (_selectedMinute / 5 == j) - { - SKPaint val6 = new SKPaint - { - Color = SelectedColor, - Style = (SKPaintStyle)0, - IsAntialias = true - }; - try - { - canvas.DrawCircle(num6, num7, 18f, val6); - val3.Color = SKColors.White; - } - finally - { - ((IDisposable)val6)?.Dispose(); - } - } - else - { - val3.Color = TextColor; - } - SKRect val7 = default(SKRect); - val3.MeasureText(num4.ToString("D2"), ref val7); - canvas.DrawText(num4.ToString("D2"), num6 - ((SKRect)(ref val7)).MidX, num7 - ((SKRect)(ref val7)).MidY, val3); - } - DrawClockHand(canvas, midX, midY, _selectedMinute * 6 - 90, 82f); - } - finally - { - ((IDisposable)val3)?.Dispose(); - } - } - finally - { - ((IDisposable)val2)?.Dispose(); - } - } - finally - { - ((IDisposable)val)?.Dispose(); - } - } + using var font = new SKFont(SKTypeface.Default, FontSize); + using var textPaint = new SKPaint(font) + { + Color = IsEnabled ? TextColor : TextColor.WithAlpha(128), + IsAntialias = true + }; + var timeText = DateTime.Today.Add(Time).ToString(Format); + var textBounds = new SKRect(); + textPaint.MeasureText(timeText, ref textBounds); + canvas.DrawText(timeText, bounds.Left + 12, bounds.MidY - textBounds.MidY, textPaint); - private void DrawClockHand(SKCanvas canvas, float centerX, float centerY, float angleDegrees, float length) - { - //IL_0018: Unknown result type (might be due to invalid IL or missing references) - //IL_001d: Unknown result type (might be due to invalid IL or missing references) - //IL_001f: Unknown result type (might be due to invalid IL or missing references) - //IL_0029: Unknown result type (might be due to invalid IL or missing references) - //IL_0030: Unknown result type (might be due to invalid IL or missing references) - //IL_003b: Unknown result type (might be due to invalid IL or missing references) - //IL_0043: Expected O, but got Unknown - double num = (double)angleDegrees * Math.PI / 180.0; - SKPaint val = new SKPaint - { - Color = SelectedColor, - Style = (SKPaintStyle)1, - StrokeWidth = 2f, - IsAntialias = true - }; - try - { - canvas.DrawLine(centerX, centerY, centerX + (float)((double)length * Math.Cos(num)), centerY + (float)((double)length * Math.Sin(num)), val); - val.Style = (SKPaintStyle)0; - canvas.DrawCircle(centerX, centerY, 6f, val); - } - finally - { - ((IDisposable)val)?.Dispose(); - } - } + DrawClockIcon(canvas, new SKRect(bounds.Right - 36, bounds.MidY - 10, bounds.Right - 12, bounds.MidY + 10)); + } - public override void OnPointerPressed(PointerEventArgs e) - { - //IL_0015: Unknown result type (might be due to invalid IL or missing references) - //IL_001a: Unknown result type (might be due to invalid IL or missing references) - //IL_001c: Unknown result type (might be due to invalid IL or missing references) - //IL_001d: Unknown result type (might be due to invalid IL or missing references) - //IL_0022: Unknown result type (might be due to invalid IL or missing references) - if (!base.IsEnabled) - { - return; - } - if (IsOpen) - { - SKRect screenBounds = base.ScreenBounds; - SKRect popupRect = GetPopupRect(screenBounds); - SKRect val = default(SKRect); - ((SKRect)(ref val))._002Ector(((SKRect)(ref popupRect)).Left, ((SKRect)(ref popupRect)).Top, ((SKRect)(ref popupRect)).Right, ((SKRect)(ref popupRect)).Top + 80f); - if (((SKRect)(ref val)).Contains(e.X, e.Y)) - { - _isSelectingHours = e.X < ((SKRect)(ref popupRect)).Left + 140f; - Invalidate(); - return; - } - float num = ((SKRect)(ref popupRect)).Left + 140f; - float num2 = ((SKRect)(ref popupRect)).Top + 80f + 140f; - float num3 = e.X - num; - float num4 = e.Y - num2; - if (Math.Sqrt(num3 * num3 + num4 * num4) <= 120.0) - { - double num5 = Math.Atan2(num4, num3) * 180.0 / Math.PI + 90.0; - if (num5 < 0.0) - { - num5 += 360.0; - } - if (_isSelectingHours) - { - _selectedHour = (int)Math.Round(num5 / 30.0) % 12; - if (_selectedHour == 0) - { - _selectedHour = 12; - } - if (Time.Hours >= 12 && _selectedHour != 12) - { - _selectedHour += 12; - } - else if (Time.Hours < 12 && _selectedHour == 12) - { - _selectedHour = 0; - } - _isSelectingHours = false; - } - else - { - _selectedMinute = (int)Math.Round(num5 / 6.0) % 60; - Time = new TimeSpan(_selectedHour, _selectedMinute, 0); - IsOpen = false; - } - Invalidate(); - return; - } - if (((SKRect)(ref screenBounds)).Contains(e.X, e.Y)) - { - IsOpen = false; - } - } - else - { - IsOpen = true; - _isSelectingHours = true; - } - Invalidate(); - } + private void DrawClockIcon(SKCanvas canvas, SKRect bounds) + { + using var paint = new SKPaint + { + Color = IsEnabled ? TextColor : TextColor.WithAlpha(128), + Style = SKPaintStyle.Stroke, + StrokeWidth = 1.5f, + IsAntialias = true + }; + var radius = Math.Min(bounds.Width, bounds.Height) / 2 - 2; + canvas.DrawCircle(bounds.MidX, bounds.MidY, radius, paint); + canvas.DrawLine(bounds.MidX, bounds.MidY, bounds.MidX, bounds.MidY - radius * 0.5f, paint); + canvas.DrawLine(bounds.MidX, bounds.MidY, bounds.MidX + radius * 0.4f, bounds.MidY, paint); + paint.Style = SKPaintStyle.Fill; + canvas.DrawCircle(bounds.MidX, bounds.MidY, 1.5f, paint); + } - public override void OnFocusLost() - { - base.OnFocusLost(); - if (IsOpen) - { - IsOpen = false; - } - } + private void DrawClockPopup(SKCanvas canvas, SKRect bounds) + { + var popupRect = GetPopupRect(bounds); - public override void OnKeyDown(KeyEventArgs e) - { - if (!base.IsEnabled) - { - return; - } - switch (e.Key) - { - case Key.Enter: - case Key.Space: - if (IsOpen) - { - if (_isSelectingHours) - { - _isSelectingHours = false; - } - else - { - Time = new TimeSpan(_selectedHour, _selectedMinute, 0); - IsOpen = false; - } - } - else - { - IsOpen = true; - _isSelectingHours = true; - } - e.Handled = true; - break; - case Key.Escape: - if (IsOpen) - { - IsOpen = false; - e.Handled = true; - } - break; - case Key.Up: - if (_isSelectingHours) - { - _selectedHour = (_selectedHour + 1) % 24; - } - else - { - _selectedMinute = (_selectedMinute + 1) % 60; - } - e.Handled = true; - break; - case Key.Down: - if (_isSelectingHours) - { - _selectedHour = (_selectedHour - 1 + 24) % 24; - } - else - { - _selectedMinute = (_selectedMinute - 1 + 60) % 60; - } - e.Handled = true; - break; - case Key.Left: - case Key.Right: - _isSelectingHours = !_isSelectingHours; - e.Handled = true; - break; - } - Invalidate(); - } + using var shadowPaint = new SKPaint { Color = new SKColor(0, 0, 0, 40), MaskFilter = SKMaskFilter.CreateBlur(SKBlurStyle.Normal, 4), Style = SKPaintStyle.Fill }; + canvas.DrawRoundRect(new SKRoundRect(new SKRect(popupRect.Left + 2, popupRect.Top + 2, popupRect.Right + 2, popupRect.Bottom + 2), CornerRadius), shadowPaint); - protected override SKSize MeasureOverride(SKSize availableSize) - { - //IL_002b: Unknown result type (might be due to invalid IL or missing references) - return new SKSize((((SKSize)(ref availableSize)).Width < float.MaxValue) ? Math.Min(((SKSize)(ref availableSize)).Width, 200f) : 200f, 40f); - } + using var bgPaint = new SKPaint { Color = ClockBackgroundColor, Style = SKPaintStyle.Fill, IsAntialias = true }; + canvas.DrawRoundRect(new SKRoundRect(popupRect, CornerRadius), bgPaint); - protected override bool HitTestPopupArea(float x, float y) - { - //IL_0001: Unknown result type (might be due to invalid IL or missing references) - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - //IL_001d: Unknown result type (might be due to invalid IL or missing references) - //IL_001e: Unknown result type (might be due to invalid IL or missing references) - //IL_0023: Unknown result type (might be due to invalid IL or missing references) - SKRect screenBounds = base.ScreenBounds; - if (((SKRect)(ref screenBounds)).Contains(x, y)) - { - return true; - } - if (_isOpen) - { - SKRect popupRect = GetPopupRect(screenBounds); - return ((SKRect)(ref popupRect)).Contains(x, y); - } - return false; - } + using var borderPaint = new SKPaint { Color = BorderColor, Style = SKPaintStyle.Stroke, StrokeWidth = 1, IsAntialias = true }; + canvas.DrawRoundRect(new SKRoundRect(popupRect, CornerRadius), borderPaint); + + DrawTimeHeader(canvas, new SKRect(popupRect.Left, popupRect.Top, popupRect.Right, popupRect.Top + HeaderHeight)); + DrawClockFace(canvas, new SKRect(popupRect.Left, popupRect.Top + HeaderHeight, popupRect.Right, popupRect.Bottom)); + } + + private void DrawTimeHeader(SKCanvas canvas, SKRect bounds) + { + using var headerPaint = new SKPaint { Color = HeaderColor, Style = SKPaintStyle.Fill }; + canvas.Save(); + canvas.ClipRoundRect(new SKRoundRect(new SKRect(bounds.Left, bounds.Top, bounds.Right, bounds.Top + CornerRadius * 2), CornerRadius)); + canvas.DrawRect(bounds, headerPaint); + canvas.Restore(); + canvas.DrawRect(new SKRect(bounds.Left, bounds.Top + CornerRadius, bounds.Right, bounds.Bottom), headerPaint); + + using var font = new SKFont(SKTypeface.Default, 32); + using var selectedPaint = new SKPaint(font) { Color = SKColors.White, IsAntialias = true }; + using var unselectedPaint = new SKPaint(font) { Color = new SKColor(255, 255, 255, 150), IsAntialias = true }; + + var hourText = _selectedHour.ToString("D2"); + var minuteText = _selectedMinute.ToString("D2"); + var hourPaint = _isSelectingHours ? selectedPaint : unselectedPaint; + var minutePaint = _isSelectingHours ? unselectedPaint : selectedPaint; + + var hourBounds = new SKRect(); var colonBounds = new SKRect(); var minuteBounds = new SKRect(); + hourPaint.MeasureText(hourText, ref hourBounds); + selectedPaint.MeasureText(":", ref colonBounds); + minutePaint.MeasureText(minuteText, ref minuteBounds); + + var totalWidth = hourBounds.Width + colonBounds.Width + minuteBounds.Width + 8; + var startX = bounds.MidX - totalWidth / 2; + var centerY = bounds.MidY - hourBounds.MidY; + + canvas.DrawText(hourText, startX, centerY, hourPaint); + canvas.DrawText(":", startX + hourBounds.Width + 4, centerY, selectedPaint); + canvas.DrawText(minuteText, startX + hourBounds.Width + colonBounds.Width + 8, centerY, minutePaint); + } + + private void DrawClockFace(SKCanvas canvas, SKRect bounds) + { + var centerX = bounds.MidX; + var centerY = bounds.MidY; + + using var facePaint = new SKPaint { Color = ClockFaceColor, Style = SKPaintStyle.Fill, IsAntialias = true }; + canvas.DrawCircle(centerX, centerY, ClockRadius + 20, facePaint); + + using var font = new SKFont(SKTypeface.Default, 14); + using var textPaint = new SKPaint(font) { Color = TextColor, IsAntialias = true }; + + if (_isSelectingHours) + { + for (int i = 1; i <= 12; i++) + { + var angle = (i * 30 - 90) * Math.PI / 180; + var x = centerX + (float)(ClockRadius * Math.Cos(angle)); + var y = centerY + (float)(ClockRadius * Math.Sin(angle)); + var isSelected = (_selectedHour % 12 == i % 12); + if (isSelected) + { + using var selBgPaint = new SKPaint { Color = SelectedColor, Style = SKPaintStyle.Fill, IsAntialias = true }; + canvas.DrawCircle(x, y, 18, selBgPaint); + textPaint.Color = SKColors.White; + } + else textPaint.Color = TextColor; + var textBounds = new SKRect(); + textPaint.MeasureText(i.ToString(), ref textBounds); + canvas.DrawText(i.ToString(), x - textBounds.MidX, y - textBounds.MidY, textPaint); + } + DrawClockHand(canvas, centerX, centerY, (_selectedHour % 12) * 30 - 90, ClockRadius - 18); + } + else + { + for (int i = 0; i < 12; i++) + { + var minute = i * 5; + var angle = (minute * 6 - 90) * Math.PI / 180; + var x = centerX + (float)(ClockRadius * Math.Cos(angle)); + var y = centerY + (float)(ClockRadius * Math.Sin(angle)); + var isSelected = (_selectedMinute / 5 == i); + if (isSelected) + { + using var selBgPaint = new SKPaint { Color = SelectedColor, Style = SKPaintStyle.Fill, IsAntialias = true }; + canvas.DrawCircle(x, y, 18, selBgPaint); + textPaint.Color = SKColors.White; + } + else textPaint.Color = TextColor; + var textBounds = new SKRect(); + textPaint.MeasureText(minute.ToString("D2"), ref textBounds); + canvas.DrawText(minute.ToString("D2"), x - textBounds.MidX, y - textBounds.MidY, textPaint); + } + DrawClockHand(canvas, centerX, centerY, _selectedMinute * 6 - 90, ClockRadius - 18); + } + } + + private void DrawClockHand(SKCanvas canvas, float centerX, float centerY, float angleDegrees, float length) + { + var angle = angleDegrees * Math.PI / 180; + using var handPaint = new SKPaint { Color = SelectedColor, Style = SKPaintStyle.Stroke, StrokeWidth = 2, IsAntialias = true }; + canvas.DrawLine(centerX, centerY, centerX + (float)(length * Math.Cos(angle)), centerY + (float)(length * Math.Sin(angle)), handPaint); + handPaint.Style = SKPaintStyle.Fill; + canvas.DrawCircle(centerX, centerY, 6, handPaint); + } + + public override void OnPointerPressed(PointerEventArgs e) + { + if (!IsEnabled) return; + + if (IsOpen) + { + // Use ScreenBounds for popup coordinate calculations (accounts for scroll offset) + var screenBounds = ScreenBounds; + var popupRect = GetPopupRect(screenBounds); + + // Check if click is in header area + var headerRect = new SKRect(popupRect.Left, popupRect.Top, popupRect.Right, popupRect.Top + HeaderHeight); + if (headerRect.Contains(e.X, e.Y)) + { + _isSelectingHours = e.X < popupRect.Left + ClockSize / 2; + Invalidate(); + return; + } + + // Check if click is in clock face area + var clockCenterX = popupRect.Left + ClockSize / 2; + var clockCenterY = popupRect.Top + HeaderHeight + ClockSize / 2; + var dx = e.X - clockCenterX; + var dy = e.Y - clockCenterY; + var distance = Math.Sqrt(dx * dx + dy * dy); + + if (distance <= ClockRadius + 20) + { + var angle = Math.Atan2(dy, dx) * 180 / Math.PI + 90; + if (angle < 0) angle += 360; + + if (_isSelectingHours) + { + _selectedHour = ((int)Math.Round(angle / 30) % 12); + if (_selectedHour == 0) _selectedHour = 12; + if (Time.Hours >= 12 && _selectedHour != 12) _selectedHour += 12; + else if (Time.Hours < 12 && _selectedHour == 12) _selectedHour = 0; + _isSelectingHours = false; + } + else + { + _selectedMinute = ((int)Math.Round(angle / 6) % 60); + Time = new TimeSpan(_selectedHour, _selectedMinute, 0); + IsOpen = false; + } + Invalidate(); + return; + } + + // Click is outside clock - check if it's on the picker itself to toggle + if (screenBounds.Contains(e.X, e.Y)) + { + IsOpen = false; + } + } + else + { + IsOpen = true; + _isSelectingHours = true; + } + Invalidate(); + } + + public override void OnFocusLost() + { + base.OnFocusLost(); + // Close popup when focus is lost (clicking outside) + if (IsOpen) + { + IsOpen = false; + } + } + + public override void OnKeyDown(KeyEventArgs e) + { + if (!IsEnabled) return; + + switch (e.Key) + { + case Key.Enter: case Key.Space: + if (IsOpen) { if (_isSelectingHours) _isSelectingHours = false; else { Time = new TimeSpan(_selectedHour, _selectedMinute, 0); IsOpen = false; } } + else { IsOpen = true; _isSelectingHours = true; } + e.Handled = true; break; + case Key.Escape: if (IsOpen) { IsOpen = false; e.Handled = true; } break; + case Key.Up: if (_isSelectingHours) _selectedHour = (_selectedHour + 1) % 24; else _selectedMinute = (_selectedMinute + 1) % 60; e.Handled = true; break; + case Key.Down: if (_isSelectingHours) _selectedHour = (_selectedHour - 1 + 24) % 24; else _selectedMinute = (_selectedMinute - 1 + 60) % 60; e.Handled = true; break; + case Key.Left: case Key.Right: _isSelectingHours = !_isSelectingHours; e.Handled = true; break; + } + Invalidate(); + } + + protected override SKSize MeasureOverride(SKSize availableSize) + { + return new SKSize(availableSize.Width < float.MaxValue ? Math.Min(availableSize.Width, 200) : 200, 40); + } + + /// + /// Override to include clock popup area in hit testing. + /// + protected override bool HitTestPopupArea(float x, float y) + { + // Use ScreenBounds for hit testing (accounts for scroll offset) + var screenBounds = ScreenBounds; + + // Always include the picker button itself + if (screenBounds.Contains(x, y)) + return true; + + // When open, also include the clock popup area (with edge detection) + if (_isOpen) + { + var popupRect = GetPopupRect(screenBounds); + return popupRect.Contains(x, y); + } + + return false; + } } diff --git a/Views/SkiaToolbarItem.cs b/Views/SkiaToolbarItem.cs deleted file mode 100644 index 5883e44..0000000 --- a/Views/SkiaToolbarItem.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Windows.Input; -using SkiaSharp; - -namespace Microsoft.Maui.Platform; - -public class SkiaToolbarItem -{ - public string Text { get; set; } = ""; - - public SKBitmap? Icon { get; set; } - - public SkiaToolbarItemOrder Order { get; set; } - - public ICommand? Command { get; set; } - - public SKRect HitBounds { get; set; } -} diff --git a/Views/SkiaToolbarItemOrder.cs b/Views/SkiaToolbarItemOrder.cs deleted file mode 100644 index 342d729..0000000 --- a/Views/SkiaToolbarItemOrder.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Microsoft.Maui.Platform; - -public enum SkiaToolbarItemOrder -{ - Primary, - Secondary -} diff --git a/Views/SkiaVerticalAlignment.cs b/Views/SkiaVerticalAlignment.cs deleted file mode 100644 index 15eafd7..0000000 --- a/Views/SkiaVerticalAlignment.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Microsoft.Maui.Platform; - -public enum SkiaVerticalAlignment -{ - Top, - Center, - Bottom -} diff --git a/Views/SkiaView.cs b/Views/SkiaView.cs index 9e14dfd..bfb4e03 100644 --- a/Views/SkiaView.cs +++ b/Views/SkiaView.cs @@ -1,933 +1,855 @@ -using System; -using System.Collections.Generic; -using Microsoft.Maui.Controls; -using Microsoft.Maui.Platform.Linux; -using Microsoft.Maui.Platform.Linux.Handlers; -using Microsoft.Maui.Platform.Linux.Rendering; -using Microsoft.Maui.Platform.Linux.Window; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using SkiaSharp; namespace Microsoft.Maui.Platform; +/// +/// Base class for all Skia-rendered views on Linux. +/// Inherits from BindableObject to enable XAML styling, data binding, and Visual State Manager. +/// public abstract class SkiaView : BindableObject, IDisposable { - private static readonly List<(SkiaView Owner, Action Draw)> _popupOverlays = new List<(SkiaView, Action)>(); - - public static readonly BindableProperty IsVisibleProperty = BindableProperty.Create("IsVisible", typeof(bool), typeof(SkiaView), (object)true, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaView)(object)b).OnVisibilityChanged(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty IsEnabledProperty = BindableProperty.Create("IsEnabled", typeof(bool), typeof(SkiaView), (object)true, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaView)(object)b).OnEnabledChanged(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty OpacityProperty = BindableProperty.Create("Opacity", typeof(float), typeof(SkiaView), (object)1f, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaView)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)((BindableObject b, object v) => Math.Clamp((float)v, 0f, 1f)), (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty BackgroundColorProperty = BindableProperty.Create("BackgroundColor", typeof(SKColor), typeof(SkiaView), (object)SKColors.Transparent, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaView)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty WidthRequestProperty = BindableProperty.Create("WidthRequest", typeof(double), typeof(SkiaView), (object)(-1.0), (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaView)(object)b).InvalidateMeasure(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty HeightRequestProperty = BindableProperty.Create("HeightRequest", typeof(double), typeof(SkiaView), (object)(-1.0), (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaView)(object)b).InvalidateMeasure(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty MinimumWidthRequestProperty = BindableProperty.Create("MinimumWidthRequest", typeof(double), typeof(SkiaView), (object)0.0, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaView)(object)b).InvalidateMeasure(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty MinimumHeightRequestProperty = BindableProperty.Create("MinimumHeightRequest", typeof(double), typeof(SkiaView), (object)0.0, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaView)(object)b).InvalidateMeasure(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty IsFocusableProperty = BindableProperty.Create("IsFocusable", typeof(bool), typeof(SkiaView), (object)false, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)null, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty MarginProperty = BindableProperty.Create("Margin", typeof(Thickness), typeof(SkiaView), (object)default(Thickness), (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaView)(object)b).InvalidateMeasure(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty HorizontalOptionsProperty = BindableProperty.Create("HorizontalOptions", typeof(LayoutOptions), typeof(SkiaView), (object)LayoutOptions.Fill, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaView)(object)b).InvalidateMeasure(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty VerticalOptionsProperty = BindableProperty.Create("VerticalOptions", typeof(LayoutOptions), typeof(SkiaView), (object)LayoutOptions.Fill, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaView)(object)b).InvalidateMeasure(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty NameProperty = BindableProperty.Create("Name", typeof(string), typeof(SkiaView), (object)string.Empty, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)null, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty ScaleProperty = BindableProperty.Create("Scale", typeof(double), typeof(SkiaView), (object)1.0, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaView)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty ScaleXProperty = BindableProperty.Create("ScaleX", typeof(double), typeof(SkiaView), (object)1.0, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaView)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty ScaleYProperty = BindableProperty.Create("ScaleY", typeof(double), typeof(SkiaView), (object)1.0, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaView)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty RotationProperty = BindableProperty.Create("Rotation", typeof(double), typeof(SkiaView), (object)0.0, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaView)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty RotationXProperty = BindableProperty.Create("RotationX", typeof(double), typeof(SkiaView), (object)0.0, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaView)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty RotationYProperty = BindableProperty.Create("RotationY", typeof(double), typeof(SkiaView), (object)0.0, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaView)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty TranslationXProperty = BindableProperty.Create("TranslationX", typeof(double), typeof(SkiaView), (object)0.0, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaView)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty TranslationYProperty = BindableProperty.Create("TranslationY", typeof(double), typeof(SkiaView), (object)0.0, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaView)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty AnchorXProperty = BindableProperty.Create("AnchorX", typeof(double), typeof(SkiaView), (object)0.5, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaView)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - public static readonly BindableProperty AnchorYProperty = BindableProperty.Create("AnchorY", typeof(double), typeof(SkiaView), (object)0.5, (BindingMode)2, (ValidateValueDelegate)null, (BindingPropertyChangedDelegate)delegate(BindableObject b, object o, object n) - { - ((SkiaView)(object)b).Invalidate(); - }, (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); - - private bool _disposed; - - private SKRect _bounds; - - private SkiaView? _parent; - - private readonly List _children = new List(); - - private SKColor _backgroundColor = SKColors.Transparent; - - public static bool HasActivePopup => _popupOverlays.Count > 0; - - public SKRect Bounds - { - get - { - //IL_0001: Unknown result type (might be due to invalid IL or missing references) - return _bounds; - } - set - { - //IL_0001: Unknown result type (might be due to invalid IL or missing references) - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - //IL_000f: Unknown result type (might be due to invalid IL or missing references) - //IL_0010: Unknown result type (might be due to invalid IL or missing references) - if (_bounds != value) - { - _bounds = value; - OnBoundsChanged(); - } - } - } - - public bool IsVisible - { - get - { - return (bool)((BindableObject)this).GetValue(IsVisibleProperty); - } - set - { - ((BindableObject)this).SetValue(IsVisibleProperty, (object)value); - } - } - - public bool IsEnabled - { - get - { - return (bool)((BindableObject)this).GetValue(IsEnabledProperty); - } - set - { - ((BindableObject)this).SetValue(IsEnabledProperty, (object)value); - } - } - - public float Opacity - { - get - { - return (float)((BindableObject)this).GetValue(OpacityProperty); - } - set - { - ((BindableObject)this).SetValue(OpacityProperty, (object)value); - } - } - - public SKColor BackgroundColor - { - get - { - //IL_0001: Unknown result type (might be due to invalid IL or missing references) - return _backgroundColor; - } - set - { - //IL_0001: Unknown result type (might be due to invalid IL or missing references) - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - //IL_000f: Unknown result type (might be due to invalid IL or missing references) - //IL_0010: Unknown result type (might be due to invalid IL or missing references) - //IL_001b: Unknown result type (might be due to invalid IL or missing references) - if (_backgroundColor != value) - { - _backgroundColor = value; - ((BindableObject)this).SetValue(BackgroundColorProperty, (object)value); - Invalidate(); - } - } - } - - public double WidthRequest - { - get - { - return (double)((BindableObject)this).GetValue(WidthRequestProperty); - } - set - { - ((BindableObject)this).SetValue(WidthRequestProperty, (object)value); - } - } - - public double HeightRequest - { - get - { - return (double)((BindableObject)this).GetValue(HeightRequestProperty); - } - set - { - ((BindableObject)this).SetValue(HeightRequestProperty, (object)value); - } - } - - public double MinimumWidthRequest - { - get - { - return (double)((BindableObject)this).GetValue(MinimumWidthRequestProperty); - } - set - { - ((BindableObject)this).SetValue(MinimumWidthRequestProperty, (object)value); - } - } - - public double MinimumHeightRequest - { - get - { - return (double)((BindableObject)this).GetValue(MinimumHeightRequestProperty); - } - set - { - ((BindableObject)this).SetValue(MinimumHeightRequestProperty, (object)value); - } - } - - public double RequestedWidth - { - get - { - return WidthRequest; - } - set - { - WidthRequest = value; - } - } - - public double RequestedHeight - { - get - { - return HeightRequest; - } - set - { - HeightRequest = value; - } - } - - public bool IsFocusable - { - get - { - return (bool)((BindableObject)this).GetValue(IsFocusableProperty); - } - set - { - ((BindableObject)this).SetValue(IsFocusableProperty, (object)value); - } - } - - public CursorType CursorType { get; set; } - - public Thickness Margin - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (Thickness)((BindableObject)this).GetValue(MarginProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(MarginProperty, (object)value); - } - } - - public LayoutOptions HorizontalOptions - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (LayoutOptions)((BindableObject)this).GetValue(HorizontalOptionsProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(HorizontalOptionsProperty, (object)value); - } - } - - public LayoutOptions VerticalOptions - { - get - { - //IL_000b: Unknown result type (might be due to invalid IL or missing references) - return (LayoutOptions)((BindableObject)this).GetValue(VerticalOptionsProperty); - } - set - { - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - ((BindableObject)this).SetValue(VerticalOptionsProperty, (object)value); - } - } - - public string Name - { - get - { - return (string)((BindableObject)this).GetValue(NameProperty); - } - set - { - ((BindableObject)this).SetValue(NameProperty, (object)value); - } - } - - public double Scale - { - get - { - return (double)((BindableObject)this).GetValue(ScaleProperty); - } - set - { - ((BindableObject)this).SetValue(ScaleProperty, (object)value); - } - } - - public double ScaleX - { - get - { - return (double)((BindableObject)this).GetValue(ScaleXProperty); - } - set - { - ((BindableObject)this).SetValue(ScaleXProperty, (object)value); - } - } - - public double ScaleY - { - get - { - return (double)((BindableObject)this).GetValue(ScaleYProperty); - } - set - { - ((BindableObject)this).SetValue(ScaleYProperty, (object)value); - } - } - - public double Rotation - { - get - { - return (double)((BindableObject)this).GetValue(RotationProperty); - } - set - { - ((BindableObject)this).SetValue(RotationProperty, (object)value); - } - } - - public double RotationX - { - get - { - return (double)((BindableObject)this).GetValue(RotationXProperty); - } - set - { - ((BindableObject)this).SetValue(RotationXProperty, (object)value); - } - } - - public double RotationY - { - get - { - return (double)((BindableObject)this).GetValue(RotationYProperty); - } - set - { - ((BindableObject)this).SetValue(RotationYProperty, (object)value); - } - } - - public double TranslationX - { - get - { - return (double)((BindableObject)this).GetValue(TranslationXProperty); - } - set - { - ((BindableObject)this).SetValue(TranslationXProperty, (object)value); - } - } - - public double TranslationY - { - get - { - return (double)((BindableObject)this).GetValue(TranslationYProperty); - } - set - { - ((BindableObject)this).SetValue(TranslationYProperty, (object)value); - } - } - - public double AnchorX - { - get - { - return (double)((BindableObject)this).GetValue(AnchorXProperty); - } - set - { - ((BindableObject)this).SetValue(AnchorXProperty, (object)value); - } - } - - public double AnchorY - { - get - { - return (double)((BindableObject)this).GetValue(AnchorYProperty); - } - set - { - ((BindableObject)this).SetValue(AnchorYProperty, (object)value); - } - } - - public bool IsFocused { get; internal set; } - - public SkiaView? Parent - { - get - { - return _parent; - } - internal set - { - _parent = value; - } - } - - public View? MauiView { get; set; } - - public SKRect ScreenBounds - { - get - { - //IL_0001: Unknown result type (might be due to invalid IL or missing references) - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - //IL_0063: Unknown result type (might be due to invalid IL or missing references) - SKRect bounds = Bounds; - for (SkiaView parent = _parent; parent != null; parent = parent.Parent) - { - if (parent is SkiaScrollView skiaScrollView) - { - ((SKRect)(ref bounds))._002Ector(((SKRect)(ref bounds)).Left - skiaScrollView.ScrollX, ((SKRect)(ref bounds)).Top - skiaScrollView.ScrollY, ((SKRect)(ref bounds)).Right - skiaScrollView.ScrollX, ((SKRect)(ref bounds)).Bottom - skiaScrollView.ScrollY); - } - } - return bounds; - } - } - - public SKSize DesiredSize { get; protected set; } - - public IReadOnlyList Children => _children; - - public event EventHandler? Invalidated; - - public static void RegisterPopupOverlay(SkiaView owner, Action drawAction) - { - _popupOverlays.RemoveAll(((SkiaView Owner, Action Draw) p) => p.Owner == owner); - _popupOverlays.Add((owner, drawAction)); - } - - public static void UnregisterPopupOverlay(SkiaView owner) - { - _popupOverlays.RemoveAll(((SkiaView Owner, Action Draw) p) => p.Owner == owner); - } - - public static void DrawPopupOverlays(SKCanvas canvas) - { - while (canvas.SaveCount > 1) - { - canvas.Restore(); - } - foreach (var popupOverlay in _popupOverlays) - { - Action item = popupOverlay.Draw; - canvas.Save(); - item(canvas); - canvas.Restore(); - } - } - - public static SkiaView? GetPopupOwnerAt(float x, float y) - { - for (int num = _popupOverlays.Count - 1; num >= 0; num--) - { - SkiaView item = _popupOverlays[num].Owner; - if (item.HitTestPopupArea(x, y)) - { - return item; - } - } - return null; - } - - protected virtual bool HitTestPopupArea(float x, float y) - { - //IL_0001: Unknown result type (might be due to invalid IL or missing references) - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - SKRect bounds = Bounds; - return ((SKRect)(ref bounds)).Contains(x, y); - } - - public SKRect GetAbsoluteBounds() - { - //IL_0001: Unknown result type (might be due to invalid IL or missing references) - //IL_0006: Unknown result type (might be due to invalid IL or missing references) - //IL_0063: Unknown result type (might be due to invalid IL or missing references) - SKRect bounds = Bounds; - for (SkiaView parent = Parent; parent != null; parent = parent.Parent) - { - if (parent is SkiaScrollView skiaScrollView) - { - ((SKRect)(ref bounds))._002Ector(((SKRect)(ref bounds)).Left - skiaScrollView.ScrollX, ((SKRect)(ref bounds)).Top - skiaScrollView.ScrollY, ((SKRect)(ref bounds)).Right - skiaScrollView.ScrollX, ((SKRect)(ref bounds)).Bottom - skiaScrollView.ScrollY); - } - } - return bounds; - } - - protected virtual void OnVisibilityChanged() - { - Invalidate(); - } - - protected virtual void OnEnabledChanged() - { - Invalidate(); - } - - protected override void OnBindingContextChanged() - { - ((BindableObject)this).OnBindingContextChanged(); - foreach (SkiaView child in _children) - { - BindableObject.SetInheritedBindingContext((BindableObject)(object)child, ((BindableObject)this).BindingContext); - } - } - - public void AddChild(SkiaView child) - { - if (child._parent != null) - { - throw new InvalidOperationException("View already has a parent"); - } - child._parent = this; - _children.Add(child); - if (((BindableObject)this).BindingContext != null) - { - BindableObject.SetInheritedBindingContext((BindableObject)(object)child, ((BindableObject)this).BindingContext); - } - Invalidate(); - } - - public void RemoveChild(SkiaView child) - { - if (child._parent == this) - { - child._parent = null; - _children.Remove(child); - Invalidate(); - } - } - - public void InsertChild(int index, SkiaView child) - { - if (child._parent != null) - { - throw new InvalidOperationException("View already has a parent"); - } - child._parent = this; - _children.Insert(index, child); - if (((BindableObject)this).BindingContext != null) - { - BindableObject.SetInheritedBindingContext((BindableObject)(object)child, ((BindableObject)this).BindingContext); - } - Invalidate(); - } - - public void ClearChildren() - { - foreach (SkiaView child in _children) - { - child._parent = null; - } - _children.Clear(); - Invalidate(); - } - - public void Invalidate() - { - //IL_0028: Unknown result type (might be due to invalid IL or missing references) - //IL_002d: Unknown result type (might be due to invalid IL or missing references) - //IL_003d: Unknown result type (might be due to invalid IL or missing references) - //IL_0042: Unknown result type (might be due to invalid IL or missing references) - //IL_005d: Unknown result type (might be due to invalid IL or missing references) - LinuxApplication.LogInvalidate(((object)this).GetType().Name); - this.Invalidated?.Invoke(this, EventArgs.Empty); - SKRect bounds = Bounds; - if (((SKRect)(ref bounds)).Width > 0f) - { - bounds = Bounds; - if (((SKRect)(ref bounds)).Height > 0f) - { - SkiaRenderingEngine.Current?.InvalidateRegion(Bounds); - } - } - if (_parent != null) - { - _parent.Invalidate(); - } - else - { - LinuxApplication.RequestRedraw(); - } - } - - public void InvalidateMeasure() - { - //IL_0001: Unknown result type (might be due to invalid IL or missing references) - DesiredSize = SKSize.Empty; - _parent?.InvalidateMeasure(); - Invalidate(); - } - - public virtual void Draw(SKCanvas canvas) - { - //IL_00b1: Unknown result type (might be due to invalid IL or missing references) - //IL_00b6: Unknown result type (might be due to invalid IL or missing references) - //IL_00c0: Unknown result type (might be due to invalid IL or missing references) - //IL_00c5: Unknown result type (might be due to invalid IL or missing references) - //IL_00da: Unknown result type (might be due to invalid IL or missing references) - //IL_00df: Unknown result type (might be due to invalid IL or missing references) - //IL_00e9: Unknown result type (might be due to invalid IL or missing references) - //IL_00ee: Unknown result type (might be due to invalid IL or missing references) - //IL_01d5: Unknown result type (might be due to invalid IL or missing references) - //IL_01da: Unknown result type (might be due to invalid IL or missing references) - //IL_01ac: Unknown result type (might be due to invalid IL or missing references) - //IL_01b1: Unknown result type (might be due to invalid IL or missing references) - //IL_01c4: Unknown result type (might be due to invalid IL or missing references) - //IL_01d3: Expected O, but got Unknown - //IL_0218: Unknown result type (might be due to invalid IL or missing references) - //IL_01e6: Unknown result type (might be due to invalid IL or missing references) - //IL_01eb: Unknown result type (might be due to invalid IL or missing references) - //IL_01ed: Unknown result type (might be due to invalid IL or missing references) - //IL_01f9: Expected O, but got Unknown - //IL_01fb: Unknown result type (might be due to invalid IL or missing references) - if (!IsVisible || Opacity <= 0f) - { - return; - } - canvas.Save(); - if (Scale != 1.0 || ScaleX != 1.0 || ScaleY != 1.0 || Rotation != 0.0 || RotationX != 0.0 || RotationY != 0.0 || TranslationX != 0.0 || TranslationY != 0.0) - { - SKRect bounds = Bounds; - float left = ((SKRect)(ref bounds)).Left; - bounds = Bounds; - float num = left + (float)((double)((SKRect)(ref bounds)).Width * AnchorX); - bounds = Bounds; - float top = ((SKRect)(ref bounds)).Top; - bounds = Bounds; - float num2 = top + (float)((double)((SKRect)(ref bounds)).Height * AnchorY); - canvas.Translate(num, num2); - if (TranslationX != 0.0 || TranslationY != 0.0) - { - canvas.Translate((float)TranslationX, (float)TranslationY); - } - if (Rotation != 0.0) - { - canvas.RotateDegrees((float)Rotation); - } - float num3 = (float)(Scale * ScaleX); - float num4 = (float)(Scale * ScaleY); - if (num3 != 1f || num4 != 1f) - { - canvas.Scale(num3, num4); - } - canvas.Translate(0f - num, 0f - num2); - } - if (Opacity < 1f) - { - canvas.SaveLayer(new SKPaint - { - Color = ((SKColor)(ref SKColors.White)).WithAlpha((byte)(Opacity * 255f)) - }); - } - if (BackgroundColor != SKColors.Transparent) - { - SKPaint val = new SKPaint - { - Color = BackgroundColor - }; - try - { - canvas.DrawRect(Bounds, val); - } - finally - { - ((IDisposable)val)?.Dispose(); - } - } - OnDraw(canvas, Bounds); - foreach (SkiaView child in _children) - { - child.Draw(canvas); - } - if (Opacity < 1f) - { - canvas.Restore(); - } - canvas.Restore(); - } - - protected virtual void OnDraw(SKCanvas canvas, SKRect bounds) - { - } - - protected virtual void OnBoundsChanged() - { - Invalidate(); - } - - public SKSize Measure(SKSize availableSize) - { - //IL_0002: Unknown result type (might be due to invalid IL or missing references) - //IL_0003: Unknown result type (might be due to invalid IL or missing references) - //IL_000e: Unknown result type (might be due to invalid IL or missing references) - DesiredSize = MeasureOverride(availableSize); - return DesiredSize; - } - - protected virtual SKSize MeasureOverride(SKSize availableSize) - { - //IL_0040: Unknown result type (might be due to invalid IL or missing references) - float num = ((WidthRequest >= 0.0) ? ((float)WidthRequest) : 0f); - float num2 = ((HeightRequest >= 0.0) ? ((float)HeightRequest) : 0f); - return new SKSize(num, num2); - } - - public virtual void Arrange(SKRect bounds) - { - //IL_0002: Unknown result type (might be due to invalid IL or missing references) - //IL_0003: Unknown result type (might be due to invalid IL or missing references) - Bounds = ArrangeOverride(bounds); - } - - protected virtual SKRect ArrangeOverride(SKRect bounds) - { - //IL_0000: Unknown result type (might be due to invalid IL or missing references) - return bounds; - } - - public virtual SkiaView? HitTest(SKPoint point) - { - return HitTest(((SKPoint)(ref point)).X, ((SKPoint)(ref point)).Y); - } - - public virtual SkiaView? HitTest(float x, float y) - { - //IL_0013: Unknown result type (might be due to invalid IL or missing references) - //IL_0018: Unknown result type (might be due to invalid IL or missing references) - if (!IsVisible || !IsEnabled) - { - return null; - } - SKRect bounds = Bounds; - if (!((SKRect)(ref bounds)).Contains(x, y)) - { - return null; - } - for (int num = _children.Count - 1; num >= 0; num--) - { - SkiaView skiaView = _children[num].HitTest(x, y); - if (skiaView != null) - { - return skiaView; - } - } - return this; - } - - public virtual void OnPointerEntered(PointerEventArgs e) - { - if (MauiView != null) - { - GestureManager.ProcessPointerEntered(MauiView, e.X, e.Y); - } - } - - public virtual void OnPointerExited(PointerEventArgs e) - { - if (MauiView != null) - { - GestureManager.ProcessPointerExited(MauiView, e.X, e.Y); - } - } - - public virtual void OnPointerMoved(PointerEventArgs e) - { - if (MauiView != null) - { - GestureManager.ProcessPointerMove(MauiView, e.X, e.Y); - } - } - - public virtual void OnPointerPressed(PointerEventArgs e) - { - if (MauiView != null) - { - GestureManager.ProcessPointerDown(MauiView, e.X, e.Y); - } - } - - public virtual void OnPointerReleased(PointerEventArgs e) - { - Console.WriteLine("[SkiaView] OnPointerReleased on " + ((object)this).GetType().Name + ", MauiView=" + (((object)MauiView)?.GetType().Name ?? "null")); - if (MauiView != null) - { - GestureManager.ProcessPointerUp(MauiView, e.X, e.Y); - } - } - - public virtual void OnScroll(ScrollEventArgs e) - { - } - - public virtual void OnKeyDown(KeyEventArgs e) - { - } - - public virtual void OnKeyUp(KeyEventArgs e) - { - } - - public virtual void OnTextInput(TextInputEventArgs e) - { - } - - public virtual void OnFocusGained() - { - IsFocused = true; - Invalidate(); - } - - public virtual void OnFocusLost() - { - IsFocused = false; - Invalidate(); - } - - protected virtual void Dispose(bool disposing) - { - if (_disposed) - { - return; - } - if (disposing) - { - foreach (SkiaView child in _children) - { - child.Dispose(); - } - _children.Clear(); - } - _disposed = true; - } - - public void Dispose() - { - Dispose(disposing: true); - GC.SuppressFinalize(this); - } + // Popup overlay system for dropdowns, calendars, etc. + private static readonly List<(SkiaView Owner, Action Draw)> _popupOverlays = new(); + + public static void RegisterPopupOverlay(SkiaView owner, Action drawAction) + { + _popupOverlays.RemoveAll(p => p.Owner == owner); + _popupOverlays.Add((owner, drawAction)); + } + + public static void UnregisterPopupOverlay(SkiaView owner) + { + _popupOverlays.RemoveAll(p => p.Owner == owner); + } + + public static void DrawPopupOverlays(SKCanvas canvas) + { + // Restore canvas to clean state for overlay drawing + // Save count tells us how many unmatched Saves there are + while (canvas.SaveCount > 1) + { + canvas.Restore(); + } + + foreach (var (_, draw) in _popupOverlays) + { + canvas.Save(); + draw(canvas); + canvas.Restore(); + } + } + + /// + /// Gets the popup owner that should receive pointer events at the given coordinates. + /// This allows popups to receive events even outside their normal bounds. + /// + public static SkiaView? GetPopupOwnerAt(float x, float y) + { + // Check in reverse order (topmost popup first) + for (int i = _popupOverlays.Count - 1; i >= 0; i--) + { + var owner = _popupOverlays[i].Owner; + if (owner.HitTestPopupArea(x, y)) + { + return owner; + } + } + return null; + } + + /// + /// Checks if there are any active popup overlays. + /// + public static bool HasActivePopup => _popupOverlays.Count > 0; + + /// + /// Override this to define the popup area for hit testing. + /// + protected virtual bool HitTestPopupArea(float x, float y) + { + // Default: no popup area beyond normal bounds + return Bounds.Contains(x, y); + } + + #region BindableProperties + + /// + /// Bindable property for IsVisible. + /// + public static readonly BindableProperty IsVisibleProperty = + BindableProperty.Create( + nameof(IsVisible), + typeof(bool), + typeof(SkiaView), + true, + propertyChanged: (b, o, n) => ((SkiaView)b).OnVisibilityChanged()); + + /// + /// Bindable property for IsEnabled. + /// + public static readonly BindableProperty IsEnabledProperty = + BindableProperty.Create( + nameof(IsEnabled), + typeof(bool), + typeof(SkiaView), + true, + propertyChanged: (b, o, n) => ((SkiaView)b).OnEnabledChanged()); + + /// + /// Bindable property for Opacity. + /// + public static readonly BindableProperty OpacityProperty = + BindableProperty.Create( + nameof(Opacity), + typeof(float), + typeof(SkiaView), + 1.0f, + coerceValue: (b, v) => Math.Clamp((float)v, 0f, 1f), + propertyChanged: (b, o, n) => ((SkiaView)b).Invalidate()); + + /// + /// Bindable property for BackgroundColor. + /// + public static readonly BindableProperty BackgroundColorProperty = + BindableProperty.Create( + nameof(BackgroundColor), + typeof(SKColor), + typeof(SkiaView), + SKColors.Transparent, + propertyChanged: (b, o, n) => ((SkiaView)b).Invalidate()); + + /// + /// Bindable property for WidthRequest. + /// + public static readonly BindableProperty WidthRequestProperty = + BindableProperty.Create( + nameof(WidthRequest), + typeof(double), + typeof(SkiaView), + -1.0, + propertyChanged: (b, o, n) => ((SkiaView)b).InvalidateMeasure()); + + /// + /// Bindable property for HeightRequest. + /// + public static readonly BindableProperty HeightRequestProperty = + BindableProperty.Create( + nameof(HeightRequest), + typeof(double), + typeof(SkiaView), + -1.0, + propertyChanged: (b, o, n) => ((SkiaView)b).InvalidateMeasure()); + + /// + /// Bindable property for MinimumWidthRequest. + /// + public static readonly BindableProperty MinimumWidthRequestProperty = + BindableProperty.Create( + nameof(MinimumWidthRequest), + typeof(double), + typeof(SkiaView), + 0.0, + propertyChanged: (b, o, n) => ((SkiaView)b).InvalidateMeasure()); + + /// + /// Bindable property for MinimumHeightRequest. + /// + public static readonly BindableProperty MinimumHeightRequestProperty = + BindableProperty.Create( + nameof(MinimumHeightRequest), + typeof(double), + typeof(SkiaView), + 0.0, + propertyChanged: (b, o, n) => ((SkiaView)b).InvalidateMeasure()); + + /// + /// Bindable property for IsFocusable. + /// + public static readonly BindableProperty IsFocusableProperty = + BindableProperty.Create( + nameof(IsFocusable), + typeof(bool), + typeof(SkiaView), + false); + + /// + /// Bindable property for Margin. + /// + public static readonly BindableProperty MarginProperty = + BindableProperty.Create( + nameof(Margin), + typeof(Thickness), + typeof(SkiaView), + default(Thickness), + propertyChanged: (b, o, n) => ((SkiaView)b).InvalidateMeasure()); + + /// + /// Bindable property for HorizontalOptions. + /// + public static readonly BindableProperty HorizontalOptionsProperty = + BindableProperty.Create( + nameof(HorizontalOptions), + typeof(LayoutOptions), + typeof(SkiaView), + LayoutOptions.Fill, + propertyChanged: (b, o, n) => ((SkiaView)b).InvalidateMeasure()); + + /// + /// Bindable property for VerticalOptions. + /// + public static readonly BindableProperty VerticalOptionsProperty = + BindableProperty.Create( + nameof(VerticalOptions), + typeof(LayoutOptions), + typeof(SkiaView), + LayoutOptions.Fill, + propertyChanged: (b, o, n) => ((SkiaView)b).InvalidateMeasure()); + + /// + /// Bindable property for Name (used for template child lookup). + /// + public static readonly BindableProperty NameProperty = + BindableProperty.Create( + nameof(Name), + typeof(string), + typeof(SkiaView), + string.Empty); + + #endregion + + private bool _disposed; + private SKRect _bounds; + private SkiaView? _parent; + private readonly List _children = new(); + + /// + /// Gets the absolute bounds of this view in screen coordinates. + /// + public SKRect GetAbsoluteBounds() + { + var bounds = Bounds; + var current = Parent; + while (current != null) + { + // Adjust for scroll offset if parent is a ScrollView + if (current is SkiaScrollView scrollView) + { + bounds = new SKRect( + bounds.Left - scrollView.ScrollX, + bounds.Top - scrollView.ScrollY, + bounds.Right - scrollView.ScrollX, + bounds.Bottom - scrollView.ScrollY); + } + current = current.Parent; + } + return bounds; + } + + /// + /// Gets or sets the bounds of this view in parent coordinates. + /// + public SKRect Bounds + { + get => _bounds; + set + { + if (_bounds != value) + { + _bounds = value; + OnBoundsChanged(); + } + } + } + + /// + /// Gets or sets whether this view is visible. + /// + public bool IsVisible + { + get => (bool)GetValue(IsVisibleProperty); + set => SetValue(IsVisibleProperty, value); + } + + /// + /// Gets or sets whether this view is enabled for interaction. + /// + public bool IsEnabled + { + get => (bool)GetValue(IsEnabledProperty); + set => SetValue(IsEnabledProperty, value); + } + + /// + /// Gets or sets the opacity of this view (0.0 to 1.0). + /// + public float Opacity + { + get => (float)GetValue(OpacityProperty); + set => SetValue(OpacityProperty, value); + } + + /// + /// Gets or sets the background color. + /// + private SKColor _backgroundColor = SKColors.Transparent; + public SKColor BackgroundColor + { + get => _backgroundColor; + set + { + if (_backgroundColor != value) + { + _backgroundColor = value; + SetValue(BackgroundColorProperty, value); // Keep BindableProperty in sync for bindings + Invalidate(); + } + } + } + + /// + /// Gets or sets the requested width. + /// + public double WidthRequest + { + get => (double)GetValue(WidthRequestProperty); + set => SetValue(WidthRequestProperty, value); + } + + /// + /// Gets or sets the requested height. + /// + public double HeightRequest + { + get => (double)GetValue(HeightRequestProperty); + set => SetValue(HeightRequestProperty, value); + } + + /// + /// Gets or sets the minimum width request. + /// + public double MinimumWidthRequest + { + get => (double)GetValue(MinimumWidthRequestProperty); + set => SetValue(MinimumWidthRequestProperty, value); + } + + /// + /// Gets or sets the minimum height request. + /// + public double MinimumHeightRequest + { + get => (double)GetValue(MinimumHeightRequestProperty); + set => SetValue(MinimumHeightRequestProperty, value); + } + + /// + /// Gets or sets the requested width (backwards compatibility alias). + /// + public double RequestedWidth + { + get => WidthRequest; + set => WidthRequest = value; + } + + /// + /// Gets or sets the requested height (backwards compatibility alias). + /// + public double RequestedHeight + { + get => HeightRequest; + set => HeightRequest = value; + } + + /// + /// Gets or sets whether this view can receive keyboard focus. + /// + public bool IsFocusable + { + get => (bool)GetValue(IsFocusableProperty); + set => SetValue(IsFocusableProperty, value); + } + + /// + /// Gets or sets the margin around this view. + /// + public Thickness Margin + { + get => (Thickness)GetValue(MarginProperty); + set => SetValue(MarginProperty, value); + } + + /// + /// Gets or sets the horizontal layout options. + /// + public LayoutOptions HorizontalOptions + { + get => (LayoutOptions)GetValue(HorizontalOptionsProperty); + set => SetValue(HorizontalOptionsProperty, value); + } + + /// + /// Gets or sets the vertical layout options. + /// + public LayoutOptions VerticalOptions + { + get => (LayoutOptions)GetValue(VerticalOptionsProperty); + set => SetValue(VerticalOptionsProperty, value); + } + + /// + /// Gets or sets the name of this view (used for template child lookup). + /// + public string Name + { + get => (string)GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + + /// + /// Gets or sets whether this view currently has keyboard focus. + /// + public bool IsFocused { get; internal set; } + + /// + /// Gets or sets the parent view. + /// + public SkiaView? Parent + { + get => _parent; + internal set => _parent = value; + } + + /// + /// Gets the bounds of this view in screen coordinates (accounting for scroll offsets). + /// + public SKRect ScreenBounds + { + get + { + var bounds = Bounds; + var parent = _parent; + + // Walk up the tree and adjust for scroll offsets + while (parent != null) + { + if (parent is SkiaScrollView scrollView) + { + bounds = new SKRect( + bounds.Left - scrollView.ScrollX, + bounds.Top - scrollView.ScrollY, + bounds.Right - scrollView.ScrollX, + bounds.Bottom - scrollView.ScrollY); + } + parent = parent.Parent; + } + + return bounds; + } + } + + /// + /// Gets the desired size calculated during measure. + /// + public SKSize DesiredSize { get; protected set; } + + /// + /// Gets the child views. + /// + public IReadOnlyList Children => _children; + + /// + /// Event raised when this view needs to be redrawn. + /// + public event EventHandler? Invalidated; + + /// + /// Called when visibility changes. + /// + protected virtual void OnVisibilityChanged() + { + Invalidate(); + } + + /// + /// Called when enabled state changes. + /// + protected virtual void OnEnabledChanged() + { + Invalidate(); + } + + /// + /// Called when binding context changes. Propagates to children. + /// + protected override void OnBindingContextChanged() + { + base.OnBindingContextChanged(); + + // Propagate binding context to children + foreach (var child in _children) + { + SetInheritedBindingContext(child, BindingContext); + } + } + + /// + /// Adds a child view. + /// + public void AddChild(SkiaView child) + { + if (child._parent != null) + throw new InvalidOperationException("View already has a parent"); + + child._parent = this; + _children.Add(child); + + // Propagate binding context to new child + if (BindingContext != null) + { + SetInheritedBindingContext(child, BindingContext); + } + + Invalidate(); + } + + /// + /// Removes a child view. + /// + public void RemoveChild(SkiaView child) + { + if (child._parent != this) + return; + + child._parent = null; + _children.Remove(child); + Invalidate(); + } + + /// + /// Inserts a child view at the specified index. + /// + public void InsertChild(int index, SkiaView child) + { + if (child._parent != null) + throw new InvalidOperationException("View already has a parent"); + + child._parent = this; + _children.Insert(index, child); + + // Propagate binding context to new child + if (BindingContext != null) + { + SetInheritedBindingContext(child, BindingContext); + } + + Invalidate(); + } + + /// + /// Removes all child views. + /// + public void ClearChildren() + { + foreach (var child in _children) + { + child._parent = null; + } + _children.Clear(); + Invalidate(); + } + + /// + /// Requests that this view be redrawn. + /// + public void Invalidate() + { + Invalidated?.Invoke(this, EventArgs.Empty); + _parent?.Invalidate(); + } + + /// + /// Invalidates the cached measurement. + /// + public void InvalidateMeasure() + { + DesiredSize = SKSize.Empty; + _parent?.InvalidateMeasure(); + Invalidate(); + } + + /// + /// Draws this view and its children to the canvas. + /// + public void Draw(SKCanvas canvas) + { + if (!IsVisible || Opacity <= 0) + { + return; + } + + canvas.Save(); + + // Apply opacity + if (Opacity < 1.0f) + { + canvas.SaveLayer(new SKPaint { Color = SKColors.White.WithAlpha((byte)(Opacity * 255)) }); + } + + // Draw background at absolute bounds + if (BackgroundColor != SKColors.Transparent) + { + using var paint = new SKPaint { Color = BackgroundColor }; + canvas.DrawRect(Bounds, paint); + } + + // Draw content at absolute bounds + OnDraw(canvas, Bounds); + + // Draw children - they draw at their own absolute bounds + foreach (var child in _children) + { + child.Draw(canvas); + } + + if (Opacity < 1.0f) + { + canvas.Restore(); + } + + canvas.Restore(); + } + + /// + /// Override to draw custom content. + /// + protected virtual void OnDraw(SKCanvas canvas, SKRect bounds) + { + } + + /// + /// Called when the bounds change. + /// + protected virtual void OnBoundsChanged() + { + Invalidate(); + } + + /// + /// Measures the desired size of this view. + /// + public SKSize Measure(SKSize availableSize) + { + DesiredSize = MeasureOverride(availableSize); + return DesiredSize; + } + + /// + /// Override to provide custom measurement. + /// + protected virtual SKSize MeasureOverride(SKSize availableSize) + { + var width = WidthRequest >= 0 ? (float)WidthRequest : 0; + var height = HeightRequest >= 0 ? (float)HeightRequest : 0; + return new SKSize(width, height); + } + + /// + /// Arranges this view within the given bounds. + /// + public void Arrange(SKRect bounds) + { + Bounds = ArrangeOverride(bounds); + } + + /// + /// Override to customize arrangement within the given bounds. + /// + protected virtual SKRect ArrangeOverride(SKRect bounds) + { + return bounds; + } + + /// + /// Performs hit testing to find the view at the given point. + /// + public virtual SkiaView? HitTest(SKPoint point) + { + return HitTest(point.X, point.Y); + } + + /// + /// Performs hit testing to find the view at the given coordinates. + /// Coordinates are in absolute window space, matching how Bounds are stored. + /// + public virtual SkiaView? HitTest(float x, float y) + { + if (!IsVisible || !IsEnabled) + return null; + + if (!Bounds.Contains(x, y)) + return null; + + // Check children in reverse order (top-most first) + // Coordinates stay in absolute space since children have absolute Bounds + for (int i = _children.Count - 1; i >= 0; i--) + { + var hit = _children[i].HitTest(x, y); + if (hit != null) + return hit; + } + + return this; + } + + #region Input Events + + public virtual void OnPointerEntered(PointerEventArgs e) { } + public virtual void OnPointerExited(PointerEventArgs e) { } + public virtual void OnPointerMoved(PointerEventArgs e) { } + public virtual void OnPointerPressed(PointerEventArgs e) { } + public virtual void OnPointerReleased(PointerEventArgs e) { } + public virtual void OnScroll(ScrollEventArgs e) { } + public virtual void OnKeyDown(KeyEventArgs e) { } + public virtual void OnKeyUp(KeyEventArgs e) { } + public virtual void OnTextInput(TextInputEventArgs e) { } + + public virtual void OnFocusGained() + { + IsFocused = true; + Invalidate(); + } + + public virtual void OnFocusLost() + { + IsFocused = false; + Invalidate(); + } + + #endregion + + #region IDisposable + + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + foreach (var child in _children) + { + child.Dispose(); + } + _children.Clear(); + } + _disposed = true; + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + #endregion +} + +/// +/// Event args for pointer events. +/// +public class PointerEventArgs : EventArgs +{ + public float X { get; } + public float Y { get; } + public PointerButton Button { get; } + public bool Handled { get; set; } + + public PointerEventArgs(float x, float y, PointerButton button = PointerButton.None) + { + X = x; + Y = y; + Button = button; + } +} + +/// +/// Mouse button flags. +/// +[Flags] +public enum PointerButton +{ + None = 0, + Left = 1, + Middle = 2, + Right = 4, + XButton1 = 8, + XButton2 = 16 +} + +/// +/// Event args for scroll events. +/// +public class ScrollEventArgs : EventArgs +{ + public float X { get; } + public float Y { get; } + public float DeltaX { get; } + public float DeltaY { get; } + public bool Handled { get; set; } + + public ScrollEventArgs(float x, float y, float deltaX, float deltaY) + { + X = x; + Y = y; + DeltaX = deltaX; + DeltaY = deltaY; + } +} + +/// +/// Event args for keyboard events. +/// +public class KeyEventArgs : EventArgs +{ + public Key Key { get; } + public KeyModifiers Modifiers { get; } + public bool Handled { get; set; } + + public KeyEventArgs(Key key, KeyModifiers modifiers = KeyModifiers.None) + { + Key = key; + Modifiers = modifiers; + } +} + +/// +/// Event args for text input events. +/// +public class TextInputEventArgs : EventArgs +{ + public string Text { get; } + public bool Handled { get; set; } + + public TextInputEventArgs(string text) + { + Text = text; + } +} + +/// +/// Keyboard modifier flags. +/// +[Flags] +public enum KeyModifiers +{ + None = 0, + Shift = 1, + Control = 2, + Alt = 4, + Super = 8, + CapsLock = 16, + NumLock = 32 } diff --git a/Views/SkiaVisualState.cs b/Views/SkiaVisualState.cs deleted file mode 100644 index 2757848..0000000 --- a/Views/SkiaVisualState.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Collections.Generic; - -namespace Microsoft.Maui.Platform; - -public class SkiaVisualState -{ - public string Name { get; set; } = ""; - - public List Setters { get; } = new List(); -} diff --git a/Views/SkiaVisualStateGroup.cs b/Views/SkiaVisualStateGroup.cs deleted file mode 100644 index 866da49..0000000 --- a/Views/SkiaVisualStateGroup.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Collections.Generic; - -namespace Microsoft.Maui.Platform; - -public class SkiaVisualStateGroup -{ - public string Name { get; set; } = ""; - - public List States { get; } = new List(); - - public SkiaVisualState? CurrentState { get; set; } -} diff --git a/Views/SkiaVisualStateGroupList.cs b/Views/SkiaVisualStateGroupList.cs deleted file mode 100644 index 1e715a0..0000000 --- a/Views/SkiaVisualStateGroupList.cs +++ /dev/null @@ -1,7 +0,0 @@ -using System.Collections.Generic; - -namespace Microsoft.Maui.Platform; - -public class SkiaVisualStateGroupList : List -{ -} diff --git a/Views/SkiaVisualStateManager.cs b/Views/SkiaVisualStateManager.cs index 06c993d..6472d8d 100644 --- a/Views/SkiaVisualStateManager.cs +++ b/Views/SkiaVisualStateManager.cs @@ -1,98 +1,216 @@ -using Microsoft.Maui.Controls; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. namespace Microsoft.Maui.Platform; +/// +/// Visual State Manager for Skia-rendered controls. +/// Provides state-based styling through XAML VisualStateGroups. +/// public static class SkiaVisualStateManager { - public static class CommonStates - { - public const string Normal = "Normal"; + /// + /// Common visual state names. + /// + public static class CommonStates + { + public const string Normal = "Normal"; + public const string Disabled = "Disabled"; + public const string Focused = "Focused"; + public const string PointerOver = "PointerOver"; + public const string Pressed = "Pressed"; + public const string Selected = "Selected"; + public const string Checked = "Checked"; + public const string Unchecked = "Unchecked"; + public const string On = "On"; + public const string Off = "Off"; + } - public const string Disabled = "Disabled"; + /// + /// Attached property for VisualStateGroups. + /// + public static readonly BindableProperty VisualStateGroupsProperty = + BindableProperty.CreateAttached( + "VisualStateGroups", + typeof(SkiaVisualStateGroupList), + typeof(SkiaVisualStateManager), + null, + propertyChanged: OnVisualStateGroupsChanged); - public const string Focused = "Focused"; + /// + /// Gets the visual state groups for the specified view. + /// + public static SkiaVisualStateGroupList? GetVisualStateGroups(SkiaView view) + { + return (SkiaVisualStateGroupList?)view.GetValue(VisualStateGroupsProperty); + } - public const string PointerOver = "PointerOver"; + /// + /// Sets the visual state groups for the specified view. + /// + public static void SetVisualStateGroups(SkiaView view, SkiaVisualStateGroupList? value) + { + view.SetValue(VisualStateGroupsProperty, value); + } - public const string Pressed = "Pressed"; + private static void OnVisualStateGroupsChanged(BindableObject bindable, object? oldValue, object? newValue) + { + if (bindable is SkiaView view && newValue is SkiaVisualStateGroupList groups) + { + // Initialize to default state + GoToState(view, CommonStates.Normal); + } + } - public const string Selected = "Selected"; + /// + /// Transitions the view to the specified visual state. + /// + /// The view to transition. + /// The name of the state to transition to. + /// True if the state was found and applied, false otherwise. + public static bool GoToState(SkiaView view, string stateName) + { + var groups = GetVisualStateGroups(view); + if (groups == null || groups.Count == 0) + return false; - public const string Checked = "Checked"; + bool stateFound = false; - public const string Unchecked = "Unchecked"; + foreach (var group in groups) + { + // Find the state in this group + SkiaVisualState? targetState = null; + foreach (var state in group.States) + { + if (state.Name == stateName) + { + targetState = state; + break; + } + } - public const string On = "On"; + if (targetState != null) + { + // Unapply current state if different + if (group.CurrentState != null && group.CurrentState != targetState) + { + UnapplyState(view, group.CurrentState); + } - public const string Off = "Off"; - } + // Apply new state + ApplyState(view, targetState); + group.CurrentState = targetState; + stateFound = true; + } + } - public static readonly BindableProperty VisualStateGroupsProperty = BindableProperty.CreateAttached("VisualStateGroups", typeof(SkiaVisualStateGroupList), typeof(SkiaVisualStateManager), (object)null, (BindingMode)2, (ValidateValueDelegate)null, new BindingPropertyChangedDelegate(OnVisualStateGroupsChanged), (BindingPropertyChangingDelegate)null, (CoerceValueDelegate)null, (CreateDefaultValueDelegate)null); + return stateFound; + } - public static SkiaVisualStateGroupList? GetVisualStateGroups(SkiaView view) - { - return (SkiaVisualStateGroupList)((BindableObject)view).GetValue(VisualStateGroupsProperty); - } + private static void ApplyState(SkiaView view, SkiaVisualState state) + { + foreach (var setter in state.Setters) + { + setter.Apply(view); + } + } - public static void SetVisualStateGroups(SkiaView view, SkiaVisualStateGroupList? value) - { - ((BindableObject)view).SetValue(VisualStateGroupsProperty, (object)value); - } - - private static void OnVisualStateGroupsChanged(BindableObject bindable, object? oldValue, object? newValue) - { - if (bindable is SkiaView view && newValue is SkiaVisualStateGroupList) - { - GoToState(view, "Normal"); - } - } - - public static bool GoToState(SkiaView view, string stateName) - { - SkiaVisualStateGroupList visualStateGroups = GetVisualStateGroups(view); - if (visualStateGroups == null || visualStateGroups.Count == 0) - { - return false; - } - bool result = false; - foreach (SkiaVisualStateGroup item in visualStateGroups) - { - SkiaVisualState skiaVisualState = null; - foreach (SkiaVisualState state in item.States) - { - if (state.Name == stateName) - { - skiaVisualState = state; - break; - } - } - if (skiaVisualState != null) - { - if (item.CurrentState != null && item.CurrentState != skiaVisualState) - { - UnapplyState(view, item.CurrentState); - } - ApplyState(view, skiaVisualState); - item.CurrentState = skiaVisualState; - result = true; - } - } - return result; - } - - private static void ApplyState(SkiaView view, SkiaVisualState state) - { - foreach (SkiaVisualStateSetter setter in state.Setters) - { - setter.Apply(view); - } - } - - private static void UnapplyState(SkiaView view, SkiaVisualState state) - { - foreach (SkiaVisualStateSetter setter in state.Setters) - { - setter.Unapply(view); - } - } + private static void UnapplyState(SkiaView view, SkiaVisualState state) + { + foreach (var setter in state.Setters) + { + setter.Unapply(view); + } + } +} + +/// +/// A list of visual state groups. +/// +public class SkiaVisualStateGroupList : List +{ +} + +/// +/// A group of mutually exclusive visual states. +/// +public class SkiaVisualStateGroup +{ + /// + /// Gets or sets the name of this group. + /// + public string Name { get; set; } = ""; + + /// + /// Gets the collection of states in this group. + /// + public List States { get; } = new(); + + /// + /// Gets or sets the currently active state. + /// + public SkiaVisualState? CurrentState { get; set; } +} + +/// +/// Represents a single visual state with its setters. +/// +public class SkiaVisualState +{ + /// + /// Gets or sets the name of this state. + /// + public string Name { get; set; } = ""; + + /// + /// Gets the collection of setters for this state. + /// + public List Setters { get; } = new(); +} + +/// +/// Sets a property value when a visual state is active. +/// +public class SkiaVisualStateSetter +{ + /// + /// Gets or sets the property to set. + /// + public BindableProperty? Property { get; set; } + + /// + /// Gets or sets the value to set. + /// + public object? Value { get; set; } + + // Store original value for unapply + private object? _originalValue; + private bool _hasOriginalValue; + + /// + /// Applies this setter to the target view. + /// + public void Apply(SkiaView view) + { + if (Property == null) return; + + // Store original value if not already stored + if (!_hasOriginalValue) + { + _originalValue = view.GetValue(Property); + _hasOriginalValue = true; + } + + view.SetValue(Property, Value); + } + + /// + /// Unapplies this setter, restoring the original value. + /// + public void Unapply(SkiaView view) + { + if (Property == null || !_hasOriginalValue) return; + + view.SetValue(Property, _originalValue); + } } diff --git a/Views/SkiaVisualStateSetter.cs b/Views/SkiaVisualStateSetter.cs deleted file mode 100644 index f964b2b..0000000 --- a/Views/SkiaVisualStateSetter.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Microsoft.Maui.Controls; - -namespace Microsoft.Maui.Platform; - -public class SkiaVisualStateSetter -{ - private object? _originalValue; - - private bool _hasOriginalValue; - - public BindableProperty? Property { get; set; } - - public object? Value { get; set; } - - public void Apply(SkiaView view) - { - if (Property != null) - { - if (!_hasOriginalValue) - { - _originalValue = ((BindableObject)view).GetValue(Property); - _hasOriginalValue = true; - } - ((BindableObject)view).SetValue(Property, Value); - } - } - - public void Unapply(SkiaView view) - { - if (Property != null && _hasOriginalValue) - { - ((BindableObject)view).SetValue(Property, _originalValue); - } - } -} diff --git a/Views/SkiaWebView.cs b/Views/SkiaWebView.cs index 3cd280e..371c9c1 100644 --- a/Views/SkiaWebView.cs +++ b/Views/SkiaWebView.cs @@ -1,1788 +1,695 @@ -using System; -using System.Collections.Generic; -using System.Runtime.CompilerServices; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using System.Runtime.InteropServices; using SkiaSharp; namespace Microsoft.Maui.Platform; +/// +/// WebView implementation using WebKitGTK for Linux. +/// Renders web content in a native GTK window and composites to Skia. +/// public class SkiaWebView : SkiaView { - private delegate void LoadChangedCallback(IntPtr webView, int loadEvent, IntPtr userData); + #region Native Interop - GTK + + private const string LibGtk4 = "libgtk-4.so.1"; + private const string LibGtk3 = "libgtk-3.so.0"; + private const string LibWebKit2Gtk4 = "libwebkitgtk-6.0.so.4"; + private const string LibWebKit2Gtk3 = "libwebkit2gtk-4.1.so.0"; + private const string LibGObject = "libgobject-2.0.so.0"; + private const string LibGLib = "libglib-2.0.so.0"; + + private static bool _useGtk4; + private static bool _gtkInitialized; + private static string _webkitLib = LibWebKit2Gtk3; + + // GTK functions + [DllImport(LibGtk4, EntryPoint = "gtk_init")] + private static extern void gtk4_init(); + + [DllImport(LibGtk3, EntryPoint = "gtk_init_check")] + private static extern bool gtk3_init_check(ref int argc, ref IntPtr argv); - private delegate IntPtr WebKitWebViewNewDelegate(); + [DllImport(LibGtk4, EntryPoint = "gtk_window_new")] + private static extern IntPtr gtk4_window_new(); - private delegate void WebKitWebViewLoadUriDelegate(IntPtr webView, [MarshalAs(UnmanagedType.LPStr)] string uri); + [DllImport(LibGtk3, EntryPoint = "gtk_window_new")] + private static extern IntPtr gtk3_window_new(int type); - private delegate void WebKitWebViewLoadHtmlDelegate(IntPtr webView, [MarshalAs(UnmanagedType.LPStr)] string html, [MarshalAs(UnmanagedType.LPStr)] string? baseUri); + [DllImport(LibGtk4, EntryPoint = "gtk_window_set_default_size")] + private static extern void gtk4_window_set_default_size(IntPtr window, int width, int height); + + [DllImport(LibGtk3, EntryPoint = "gtk_window_set_default_size")] + private static extern void gtk3_window_set_default_size(IntPtr window, int width, int height); - private delegate IntPtr WebKitWebViewGetUriDelegate(IntPtr webView); + [DllImport(LibGtk4, EntryPoint = "gtk_window_set_child")] + private static extern void gtk4_window_set_child(IntPtr window, IntPtr child); - private delegate IntPtr WebKitWebViewGetTitleDelegate(IntPtr webView); + [DllImport(LibGtk3, EntryPoint = "gtk_container_add")] + private static extern void gtk3_container_add(IntPtr container, IntPtr widget); - 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); - - 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; - } - - private const string LibGtk4 = "libgtk-4.so.1"; - - private const string LibGtk3 = "libgtk-3.so.0"; - - private const string LibWebKit2Gtk4 = "libwebkitgtk-6.0.so.4"; - - private const string LibWebKit2Gtk3 = "libwebkit2gtk-4.1.so.0"; - - private const string LibGObject = "libgobject-2.0.so.0"; - - private const string LibGLib = "libglib-2.0.so.0"; - - private static bool _useGtk4; - - private static bool _gtkInitialized; - - private static string _webkitLib = "libwebkit2gtk-4.1.so.0"; - - private static LoadChangedCallback? _loadChangedCallback; - - private const string LibGdk4 = "libgtk-4.so.1"; - - private const string LibGdk3 = "libgdk-3.so.0"; - - private const string LibX11 = "libX11.so.6"; - - 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 const int RTLD_NOW = 2; - - private const int RTLD_GLOBAL = 256; - - private static IntPtr _webkitHandle; - - 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 static IntPtr _mainDisplay; - - private static IntPtr _mainWindow; - - private static readonly List _activeWebViews = new List(); - - private static readonly Dictionary _webViewInstances = new Dictionary(); - - private int _lastPosX; - - private int _lastPosY; - - private int _lastWidth; - - private int _lastHeight; - - public string Source - { - get - { - return _source; - } - set - { - if (!(_source != value)) - { - return; - } - _source = value; - if (!string.IsNullOrEmpty(value)) - { - if (!_isInitialized) - { - Initialize(); - } - if (_isInitialized) - { - LoadUrl(value); - } - } - Invalidate(); - } - } - - public string Html - { - get - { - return _html; - } - set - { - if (!(_html != value)) - { - return; - } - _html = value; - if (!string.IsNullOrEmpty(value)) - { - if (!_isInitialized) - { - Initialize(); - } - if (_isInitialized) - { - LoadHtml(value); - } - } - Invalidate(); - } - } - - public bool CanGoBack - { - get - { - if (_webView != IntPtr.Zero) - { - return _webkitCanGoBack?.Invoke(_webView) ?? false; - } - return false; - } - } - - public bool CanGoForward - { - get - { - if (_webView != IntPtr.Zero) - { - return _webkitCanGoForward?.Invoke(_webView) ?? false; - } - return false; - } - } - - public string? CurrentUrl - { - get - { - if (_webView == IntPtr.Zero || _webkitGetUri == null) - { - return null; - } - IntPtr intPtr = _webkitGetUri(_webView); - if (intPtr == IntPtr.Zero) - { - return null; - } - return Marshal.PtrToStringAnsi(intPtr); - } - } - - public string? Title - { - get - { - if (_webView == IntPtr.Zero || _webkitGetTitle == null) - { - return null; - } - IntPtr intPtr = _webkitGetTitle(_webView); - if (intPtr == IntPtr.Zero) - { - return null; - } - return Marshal.PtrToStringAnsi(intPtr); - } - } - - public bool JavaScriptEnabled - { - get - { - return _javascriptEnabled; - } - set - { - _javascriptEnabled = value; - UpdateJavaScriptSetting(); - } - } - - public double LoadProgress => _loadProgress; - - public static bool IsSupported => InitializeWebKit(); - - public event EventHandler? Navigating; - - public event EventHandler? Navigated; - - public event EventHandler? TitleChanged; - - public event EventHandler? LoadProgressChanged; - - [DllImport("libgtk-4.so.1", EntryPoint = "gtk_init")] - private static extern void gtk4_init(); - - [DllImport("libgtk-3.so.0", EntryPoint = "gtk_init_check")] - private static extern bool gtk3_init_check(ref int argc, ref IntPtr argv); - - [DllImport("libgtk-4.so.1", EntryPoint = "gtk_window_new")] - private static extern IntPtr gtk4_window_new(); - - [DllImport("libgtk-3.so.0", EntryPoint = "gtk_window_new")] - private static extern IntPtr gtk3_window_new(int type); - - [DllImport("libgtk-4.so.1", EntryPoint = "gtk_window_set_default_size")] - private static extern void gtk4_window_set_default_size(IntPtr window, int width, int height); - - [DllImport("libgtk-4.so.1", EntryPoint = "gtk_window_set_title")] - private static extern void gtk4_window_set_title(IntPtr window, [MarshalAs(UnmanagedType.LPStr)] string title); - - [DllImport("libgtk-3.so.0", EntryPoint = "gtk_window_set_default_size")] - private static extern void gtk3_window_set_default_size(IntPtr window, int width, int height); - - [DllImport("libgtk-3.so.0", EntryPoint = "gtk_window_set_title")] - private static extern void gtk3_window_set_title(IntPtr window, [MarshalAs(UnmanagedType.LPStr)] string title); - - [DllImport("libgtk-4.so.1", EntryPoint = "gtk_window_set_child")] - private static extern void gtk4_window_set_child(IntPtr window, IntPtr child); - - [DllImport("libgtk-3.so.0", EntryPoint = "gtk_container_add")] - private static extern void gtk3_container_add(IntPtr container, IntPtr widget); - - [DllImport("libgtk-4.so.1", EntryPoint = "gtk_widget_show")] - private static extern void gtk4_widget_show(IntPtr widget); - - [DllImport("libgtk-4.so.1", EntryPoint = "gtk_window_present")] - private static extern void gtk4_window_present(IntPtr window); - - [DllImport("libgtk-3.so.0", EntryPoint = "gtk_widget_show_all")] - private static extern void gtk3_widget_show_all(IntPtr widget); - - [DllImport("libgtk-4.so.1", EntryPoint = "gtk_widget_hide")] - private static extern void gtk4_widget_hide(IntPtr widget); - - [DllImport("libgtk-3.so.0", EntryPoint = "gtk_widget_hide")] - private static extern void gtk3_widget_hide(IntPtr widget); - - [DllImport("libgtk-4.so.1", EntryPoint = "gtk_widget_get_width")] - private static extern int gtk4_widget_get_width(IntPtr widget); - - [DllImport("libgtk-4.so.1", EntryPoint = "gtk_widget_get_height")] - private static extern int gtk4_widget_get_height(IntPtr widget); - - [DllImport("libgobject-2.0.so.0")] - private static extern void g_object_unref(IntPtr obj); - - [DllImport("libgobject-2.0.so.0")] - private static extern ulong g_signal_connect_data(IntPtr instance, [MarshalAs(UnmanagedType.LPStr)] string signal, IntPtr handler, IntPtr data, IntPtr destroyData, int flags); - - [DllImport("libgtk-4.so.1", EntryPoint = "gtk_native_get_surface")] - private static extern IntPtr gtk4_native_get_surface(IntPtr native); - - [DllImport("libgtk-4.so.1", EntryPoint = "gdk_x11_surface_get_xid")] - private static extern IntPtr gdk4_x11_surface_get_xid(IntPtr surface); - - [DllImport("libgtk-3.so.0", EntryPoint = "gtk_widget_get_window")] - private static extern IntPtr gtk3_widget_get_window(IntPtr widget); - - [DllImport("libgdk-3.so.0", EntryPoint = "gdk_x11_window_get_xid")] - private static extern IntPtr gdk3_x11_window_get_xid(IntPtr gdkWindow); - - [DllImport("libX11.so.6")] - private static extern int XReparentWindow(IntPtr display, IntPtr window, IntPtr parent, int x, int y); - - [DllImport("libX11.so.6")] - private static extern int XMoveResizeWindow(IntPtr display, IntPtr window, int x, int y, uint width, uint height); - - [DllImport("libX11.so.6")] - private static extern int XMapWindow(IntPtr display, IntPtr window); - - [DllImport("libX11.so.6")] - private static extern int XUnmapWindow(IntPtr display, IntPtr window); - - [DllImport("libX11.so.6")] - private static extern int XFlush(IntPtr display); - - [DllImport("libX11.so.6")] - private static extern int XRaiseWindow(IntPtr display, IntPtr window); - - [DllImport("libX11.so.6")] - 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.so.6")] - private static extern int XDestroyWindow(IntPtr display, IntPtr window); - - [DllImport("libX11.so.6")] - private static extern int XSelectInput(IntPtr display, IntPtr window, long eventMask); - - [DllImport("libX11.so.6")] - private static extern int XSync(IntPtr display, bool discard); - - [DllImport("libX11.so.6")] - private static extern bool XQueryTree(IntPtr display, IntPtr window, ref IntPtr root, ref IntPtr parent, ref IntPtr children, ref uint nchildren); - - [DllImport("libX11.so.6")] - private static extern int XFree(IntPtr data); - - [DllImport("libgtk-4.so.1", EntryPoint = "gtk_window_set_decorated")] - private static extern void gtk4_window_set_decorated(IntPtr window, bool decorated); - - [DllImport("libgtk-3.so.0", EntryPoint = "gtk_window_set_decorated")] - private static extern void gtk3_window_set_decorated(IntPtr window, bool decorated); - - [DllImport("libgtk-3.so.0", EntryPoint = "gtk_window_move")] - private static extern void gtk3_window_move(IntPtr window, int x, int y); - - [DllImport("libgtk-3.so.0", EntryPoint = "gtk_window_resize")] - private static extern void gtk3_window_resize(IntPtr window, int width, int height); - - [DllImport("libgdk-3.so.0", EntryPoint = "gdk_window_move_resize")] - private static extern void gdk3_window_move_resize(IntPtr window, int x, int y, int width, int height); - - [DllImport("libgdk-3.so.0", EntryPoint = "gdk_window_move")] - private static extern void gdk3_gdk_window_move(IntPtr window, int x, int y); - - [DllImport("libgdk-3.so.0", EntryPoint = "gdk_window_set_override_redirect")] - private static extern void gdk3_window_set_override_redirect(IntPtr window, bool override_redirect); - - [DllImport("libgdk-3.so.0", EntryPoint = "gdk_window_set_transient_for")] - private static extern void gdk3_window_set_transient_for(IntPtr window, IntPtr parent); - - [DllImport("libgdk-3.so.0", EntryPoint = "gdk_window_raise")] - private static extern void gdk3_window_raise(IntPtr window); - - [DllImport("libgdk-3.so.0", EntryPoint = "gdk_x11_window_foreign_new_for_display")] - private static extern IntPtr gdk3_x11_window_foreign_new_for_display(IntPtr display, IntPtr window); - - [DllImport("libgdk-3.so.0", EntryPoint = "gdk_display_get_default")] - private static extern IntPtr gdk3_display_get_default(); - - [DllImport("libgtk-3.so.0", EntryPoint = "gtk_widget_set_parent_window")] - private static extern void gtk3_widget_set_parent_window(IntPtr widget, IntPtr parentWindow); - - [DllImport("libgtk-3.so.0", EntryPoint = "gtk_socket_new")] - private static extern IntPtr gtk3_socket_new(); - - [DllImport("libgtk-3.so.0", EntryPoint = "gtk_socket_add_id")] - private static extern void gtk3_socket_add_id(IntPtr socket, IntPtr windowId); - - [DllImport("libgtk-3.so.0", EntryPoint = "gtk_socket_get_id")] - private static extern IntPtr gtk3_socket_get_id(IntPtr socket); - - [DllImport("libgtk-3.so.0", EntryPoint = "gtk_plug_new")] - private static extern IntPtr gtk3_plug_new(IntPtr socketId); - - [DllImport("libgtk-3.so.0", EntryPoint = "gtk_plug_get_id")] - private static extern IntPtr gtk3_plug_get_id(IntPtr plug); - - [DllImport("libgtk-3.so.0", EntryPoint = "gtk_window_set_skip_taskbar_hint")] - private static extern void gtk3_window_set_skip_taskbar_hint(IntPtr window, bool setting); - - [DllImport("libgtk-3.so.0", EntryPoint = "gtk_window_set_skip_pager_hint")] - private static extern void gtk3_window_set_skip_pager_hint(IntPtr window, bool setting); - - [DllImport("libgtk-3.so.0", EntryPoint = "gtk_window_set_type_hint")] - private static extern void gtk3_window_set_type_hint(IntPtr window, int hint); - - [DllImport("libgtk-3.so.0", EntryPoint = "gtk_window_present")] - private static extern void gtk3_window_present(IntPtr window); - - [DllImport("libgtk-3.so.0", EntryPoint = "gtk_widget_queue_draw")] - private static extern void gtk3_widget_queue_draw(IntPtr widget); - - [DllImport("libgtk-3.so.0", EntryPoint = "gtk_window_set_keep_above")] - private static extern void gtk3_window_set_keep_above(IntPtr window, bool setting); - - [DllImport("libgtk-3.so.0", EntryPoint = "gtk_widget_set_hexpand")] - private static extern void gtk3_widget_set_hexpand(IntPtr widget, bool expand); - - [DllImport("libgtk-3.so.0", EntryPoint = "gtk_widget_set_vexpand")] - private static extern void gtk3_widget_set_vexpand(IntPtr widget, bool expand); - - [DllImport("libgtk-3.so.0", EntryPoint = "gtk_widget_set_size_request")] - private static extern void gtk3_widget_set_size_request(IntPtr widget, int width, int height); - - [DllImport("libgtk-3.so.0", EntryPoint = "gtk_widget_realize")] - private static extern void gtk3_widget_realize(IntPtr widget); - - [DllImport("libgtk-3.so.0", EntryPoint = "gtk_widget_map")] - private static extern void gtk3_widget_map(IntPtr widget); - - [DllImport("libglib-2.0.so.0")] - private static extern bool g_main_context_iteration(IntPtr context, bool mayBlock); - - [DllImport("libgtk-3.so.0", EntryPoint = "gtk_events_pending")] - private static extern bool gtk3_events_pending(); - - [DllImport("libgtk-3.so.0", EntryPoint = "gtk_main_iteration")] - private static extern void gtk3_main_iteration(); - - [DllImport("libgtk-3.so.0", EntryPoint = "gtk_main_iteration_do")] - private static extern bool gtk3_main_iteration_do(bool blocking); - - [DllImport("libdl.so.2")] - private static extern IntPtr dlopen([MarshalAs(UnmanagedType.LPStr)] string? filename, int flags); - - [DllImport("libdl.so.2")] - private static extern IntPtr dlsym(IntPtr handle, [MarshalAs(UnmanagedType.LPStr)] string symbol); - - [DllImport("libdl.so.2")] - private static extern IntPtr dlerror(); - - public static void SetMainWindow(IntPtr display, IntPtr window) - { - _mainDisplay = display; - _mainWindow = window; - Console.WriteLine($"[WebView] Main window set: display={display}, window={window}"); - } - - public SkiaWebView() - { - //IL_0042: Unknown result type (might be due to invalid IL or missing references) - base.RequestedWidth = 400.0; - base.RequestedHeight = 300.0; - base.BackgroundColor = SKColors.White; - } - - private static bool InitializeWebKit() - { - if (_webkitHandle != IntPtr.Zero) - { - return true; - } - _webkitHandle = dlopen("libwebkit2gtk-4.1.so.0", 258); - if (_webkitHandle != IntPtr.Zero) - { - _useGtk4 = false; - _webkitLib = "libwebkit2gtk-4.1.so.0"; - } - else - { - _webkitHandle = dlopen("libwebkit2gtk-4.0.so.37", 258); - if (_webkitHandle != IntPtr.Zero) - { - _useGtk4 = false; - _webkitLib = "libwebkit2gtk-4.0.so.37"; - } - else - { - _webkitHandle = dlopen("libwebkitgtk-6.0.so.4", 258); - if (_webkitHandle != IntPtr.Zero) - { - _useGtk4 = true; - _webkitLib = "libwebkitgtk-6.0.so.4"; - Console.WriteLine("[WebView] Warning: Using GTK4 WebKitGTK - embedding may be limited"); - } - } - } - if (_webkitHandle == IntPtr.Zero) - { - Console.WriteLine("[WebView] WebKitGTK not found. Install with: sudo apt install libwebkit2gtk-4.1-0"); - return false; - } - _webkitWebViewNew = LoadFunction("webkit_web_view_new"); - _webkitLoadUri = LoadFunction("webkit_web_view_load_uri"); - _webkitLoadHtml = LoadFunction("webkit_web_view_load_html"); - _webkitGetUri = LoadFunction("webkit_web_view_get_uri"); - _webkitGetTitle = LoadFunction("webkit_web_view_get_title"); - _webkitGoBack = LoadFunction("webkit_web_view_go_back"); - _webkitGoForward = LoadFunction("webkit_web_view_go_forward"); - _webkitCanGoBack = LoadFunction("webkit_web_view_can_go_back"); - _webkitCanGoForward = LoadFunction("webkit_web_view_can_go_forward"); - _webkitReload = LoadFunction("webkit_web_view_reload"); - _webkitStopLoading = LoadFunction("webkit_web_view_stop_loading"); - _webkitGetProgress = LoadFunction("webkit_web_view_get_estimated_load_progress"); - _webkitGetSettings = LoadFunction("webkit_web_view_get_settings"); - _webkitSetJavascript = LoadFunction("webkit_settings_set_enable_javascript"); - _webkitSetHardwareAcceleration = LoadFunction("webkit_settings_set_hardware_acceleration_policy"); - _webkitSetWebgl = LoadFunction("webkit_settings_set_enable_webgl"); - Console.WriteLine("[WebView] Using " + _webkitLib); - return _webkitWebViewNew != null; - } - - private static T? LoadFunction(string name) where T : Delegate - { - IntPtr intPtr = dlsym(_webkitHandle, name); - if (intPtr == IntPtr.Zero) - { - return null; - } - return Marshal.GetDelegateForFunctionPointer(intPtr); - } - - private void Initialize() - { - if (_isInitialized || !InitializeWebKit()) - { - return; - } - try - { - 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"); - Environment.GetEnvironmentVariable("DISPLAY"); - string environmentVariable = 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=" + environmentVariable + ", XDG_SESSION_TYPE=x11"); - if (_useGtk4) - { - gtk4_init(); - } - else - { - int argc = 0; - IntPtr argv = IntPtr.Zero; - if (!gtk3_init_check(ref argc, ref argv)) - { - Console.WriteLine("[WebView] gtk3_init_check failed!"); - } - } - _gtkInitialized = true; - IntPtr value = gdk3_display_get_default(); - Console.WriteLine($"[WebView] GDK display: {value}"); - } - _webView = _webkitWebViewNew(); - if (_webView == IntPtr.Zero) - { - Console.WriteLine("[WebView] Failed to create WebKit view"); - return; - } - _webViewInstances[_webView] = this; - _loadChangedCallback = OnLoadChanged; - IntPtr functionPointerForDelegate = Marshal.GetFunctionPointerForDelegate(_loadChangedCallback); - g_signal_connect_data(_webView, "load-changed", functionPointerForDelegate, IntPtr.Zero, IntPtr.Zero, 0); - Console.WriteLine("[WebView] Connected to load-changed signal"); - int num = Math.Max(800, (int)base.RequestedWidth); - int num2 = Math.Max(600, (int)base.RequestedHeight); - if (_useGtk4) - { - _gtkWindow = gtk4_window_new(); - gtk4_window_set_title(_gtkWindow, "OpenMaui WebView"); - gtk4_window_set_default_size(_gtkWindow, num, num2); - gtk4_window_set_child(_gtkWindow, _webView); - Console.WriteLine($"[WebView] GTK4 window created: {num}x{num2}"); - } - else - { - _gtkWindow = gtk3_window_new(0); - gtk3_window_set_default_size(_gtkWindow, num, num2); - gtk3_window_set_title(_gtkWindow, "WebViewDemo"); - gtk3_widget_set_hexpand(_webView, expand: true); - gtk3_widget_set_vexpand(_webView, expand: true); - gtk3_widget_set_size_request(_webView, num, num2); - gtk3_container_add(_gtkWindow, _webView); - Console.WriteLine($"[WebView] GTK3 TOPLEVEL window created: {num}x{num2}"); - } - ConfigureWebKitSettings(); - UpdateJavaScriptSetting(); - _isInitialized = true; - lock (_activeWebViews) - { - if (!_activeWebViews.Contains(this)) - { - _activeWebViews.Add(this); - } - } - if (!string.IsNullOrEmpty(_source)) - { - LoadUrl(_source); - } - else if (!string.IsNullOrEmpty(_html)) - { - LoadHtml(_html); - } - Console.WriteLine("[WebView] Initialized successfully"); - } - catch (Exception ex) - { - Console.WriteLine("[WebView] Initialization failed: " + ex.Message); - } - } - - public void LoadUrl(string url) - { - if (!_isInitialized) - { - Initialize(); - } - if (_webView != IntPtr.Zero && _webkitLoadUri != null) - { - this.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) - { - 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() - { - if (_webView != IntPtr.Zero && CanGoBack) - { - _webkitGoBack?.Invoke(_webView); - } - } - - public void GoForward() - { - if (_webView != IntPtr.Zero && CanGoForward) - { - _webkitGoForward?.Invoke(_webView); - } - } - - public void Reload() - { - if (_webView != IntPtr.Zero) - { - _webkitReload?.Invoke(_webView); - } - } - - public void Stop() - { - if (_webView != IntPtr.Zero) - { - _webkitStopLoading?.Invoke(_webView); - } - } - - private void UpdateJavaScriptSetting() - { - if (_webView != IntPtr.Zero && _webkitGetSettings != null && _webkitSetJavascript != null) - { - IntPtr intPtr = _webkitGetSettings(_webView); - if (intPtr != IntPtr.Zero) - { - _webkitSetJavascript(intPtr, _javascriptEnabled); - } - } - } - - private void ConfigureWebKitSettings() - { - if (_webView == IntPtr.Zero) - { - return; - } - try - { - if (_webkitGetSettings == null) - { - return; - } - IntPtr intPtr = _webkitGetSettings(_webView); - if (intPtr == IntPtr.Zero) - { - Console.WriteLine("[WebView] Could not get WebKit settings"); - return; - } - if (_webkitSetHardwareAcceleration != null) - { - _webkitSetHardwareAcceleration(intPtr, 2); - 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(intPtr, enabled: 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 static void OnLoadChanged(IntPtr webView, int loadEvent, IntPtr userData) - { - string[] array = new string[4] { "STARTED", "REDIRECTED", "COMMITTED", "FINISHED" }; - string text = ((loadEvent >= 0 && loadEvent < array.Length) ? array[loadEvent] : loadEvent.ToString()); - Console.WriteLine("[WebView] Load event: " + text); - if (!_webViewInstances.TryGetValue(webView, out SkiaWebView value)) - { - return; - } - string url = value.Source ?? ""; - if (_webkitGetUri != null) - { - IntPtr intPtr = _webkitGetUri(webView); - if (intPtr != IntPtr.Zero) - { - url = Marshal.PtrToStringAnsi(intPtr) ?? ""; - } - } - switch (loadEvent) - { - case 0: - value.Navigating?.Invoke(value, new WebNavigatingEventArgs(url)); - break; - case 3: - value.Navigated?.Invoke(value, new WebNavigatedEventArgs(url, success: true)); - break; - } - } - - public void ProcessEvents() - { - if (!_isInitialized) - { - return; - } - g_main_context_iteration(IntPtr.Zero, mayBlock: false); - if (_webView != IntPtr.Zero && _webkitGetProgress != null) - { - double num = _webkitGetProgress(_webView); - if (Math.Abs(num - _loadProgress) > 0.01) - { - _loadProgress = num; - this.LoadProgressChanged?.Invoke(this, num); - } - } - } - - private bool CreateX11Container() - { - //IL_003f: Unknown result type (might be due to invalid IL or missing references) - //IL_0044: Unknown result type (might be due to invalid IL or missing references) - //IL_0050: Unknown result type (might be due to invalid IL or missing references) - //IL_0055: Unknown result type (might be due to invalid IL or missing references) - //IL_0063: Unknown result type (might be due to invalid IL or missing references) - //IL_0068: Unknown result type (might be due to invalid IL or missing references) - //IL_007b: Unknown result type (might be due to invalid IL or missing references) - //IL_0080: Unknown result type (might be due to invalid IL or missing references) - 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 - { - SKRect bounds = base.Bounds; - int num = (int)((SKRect)(ref bounds)).Left; - bounds = base.Bounds; - int num2 = (int)((SKRect)(ref bounds)).Top; - bounds = base.Bounds; - uint num3 = Math.Max(100u, (uint)((SKRect)(ref bounds)).Width); - bounds = base.Bounds; - uint num4 = Math.Max(100u, (uint)((SKRect)(ref bounds)).Height); - if (num3 < 100) - { - num3 = 780u; - } - if (num4 < 100) - { - num4 = 300u; - } - Console.WriteLine($"[WebView] Creating X11 container at ({num}, {num2}), size ({num3}x{num4})"); - _x11Container = XCreateSimpleWindow(_mainDisplay, _mainWindow, num, num2, num3, num4, 0u, 0uL, 16777215uL); - 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, decorated: false); - gtk3_window_set_skip_taskbar_hint(_gtkWindow, setting: true); - gtk3_window_set_skip_pager_hint(_gtkWindow, setting: true); - gtk3_window_set_keep_above(_gtkWindow, setting: true); - gtk3_window_set_type_hint(_gtkWindow, 5); - } - 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(blocking: 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; - } - IntPtr intPtr = gtk3_widget_get_window(_gtkWindow); - if (intPtr != IntPtr.Zero) - { - _gtkX11Window = gdk3_x11_window_get_xid(intPtr); - Console.WriteLine($"[WebView] GTK X11 window: {_gtkX11Window}"); - } - PositionUsingGtk(); - } - - private void PositionUsingGtk() - { - //IL_004f: Unknown result type (might be due to invalid IL or missing references) - //IL_0054: Unknown result type (might be due to invalid IL or missing references) - //IL_0062: Unknown result type (might be due to invalid IL or missing references) - //IL_0067: Unknown result type (might be due to invalid IL or missing references) - //IL_0076: Unknown result type (might be due to invalid IL or missing references) - //IL_007b: Unknown result type (might be due to invalid IL or missing references) - //IL_008f: Unknown result type (might be due to invalid IL or missing references) - //IL_0094: Unknown result type (might be due to invalid IL or missing references) - //IL_0110: Unknown result type (might be due to invalid IL or missing references) - //IL_0115: Unknown result type (might be due to invalid IL or missing references) - //IL_0132: Unknown result type (might be due to invalid IL or missing references) - //IL_0137: Unknown result type (might be due to invalid IL or missing references) - if (_gtkWindow == IntPtr.Zero || _mainDisplay == IntPtr.Zero) - { - return; - } - int destX = 0; - int destY = 0; - try - { - IntPtr dest = XDefaultRootWindow(_mainDisplay); - XTranslateCoordinates(_mainDisplay, _mainWindow, dest, 0, 0, out destX, out destY, out var _); - } - catch - { - destX = 0; - destY = 0; - } - int num = destX; - SKRect bounds = base.Bounds; - int num2 = num + (int)((SKRect)(ref bounds)).Left; - int num3 = destY; - bounds = base.Bounds; - int num4 = num3 + (int)((SKRect)(ref bounds)).Top; - bounds = base.Bounds; - int num5 = Math.Max(100, (int)((SKRect)(ref bounds)).Width); - bounds = base.Bounds; - int num6 = Math.Max(100, (int)((SKRect)(ref bounds)).Height); - DefaultInterpolatedStringHandler defaultInterpolatedStringHandler = new DefaultInterpolatedStringHandler(53, 6); - defaultInterpolatedStringHandler.AppendLiteral("[WebView] Position: screen=("); - defaultInterpolatedStringHandler.AppendFormatted(num2); - defaultInterpolatedStringHandler.AppendLiteral(", "); - defaultInterpolatedStringHandler.AppendFormatted(num4); - defaultInterpolatedStringHandler.AppendLiteral("), size ("); - defaultInterpolatedStringHandler.AppendFormatted(num5); - defaultInterpolatedStringHandler.AppendLiteral("x"); - defaultInterpolatedStringHandler.AppendFormatted(num6); - defaultInterpolatedStringHandler.AppendLiteral("), bounds=("); - bounds = base.Bounds; - defaultInterpolatedStringHandler.AppendFormatted(((SKRect)(ref bounds)).Left); - defaultInterpolatedStringHandler.AppendLiteral(","); - bounds = base.Bounds; - defaultInterpolatedStringHandler.AppendFormatted(((SKRect)(ref bounds)).Top); - defaultInterpolatedStringHandler.AppendLiteral(")"); - Console.WriteLine(defaultInterpolatedStringHandler.ToStringAndClear()); - if (!_useGtk4) - { - gtk3_window_move(_gtkWindow, num2, num4); - gtk3_window_resize(_gtkWindow, num5, num6); - while (gtk3_events_pending()) - { - gtk3_main_iteration_do(blocking: false); - } - if (_gtkX11Window != IntPtr.Zero) - { - XRaiseWindow(_mainDisplay, _gtkX11Window); - SetWindowAlwaysOnTop(_gtkX11Window); - XFlush(_mainDisplay); - } - } - else - { - gtk4_window_set_default_size(_gtkWindow, num5, num6); - } - } - - private void PositionWithX11() - { - //IL_004b: Unknown result type (might be due to invalid IL or missing references) - //IL_0050: Unknown result type (might be due to invalid IL or missing references) - //IL_005e: Unknown result type (might be due to invalid IL or missing references) - //IL_0063: Unknown result type (might be due to invalid IL or missing references) - //IL_0075: Unknown result type (might be due to invalid IL or missing references) - //IL_007a: Unknown result type (might be due to invalid IL or missing references) - //IL_0092: Unknown result type (might be due to invalid IL or missing references) - //IL_0097: Unknown result type (might be due to invalid IL or missing references) - //IL_00ae: Unknown result type (might be due to invalid IL or missing references) - //IL_00b3: Unknown result type (might be due to invalid IL or missing references) - //IL_00cb: Unknown result type (might be due to invalid IL or missing references) - //IL_00d0: Unknown result type (might be due to invalid IL or missing references) - if (_gtkX11Window != IntPtr.Zero && _mainDisplay != IntPtr.Zero) - { - int destX = 0; - int destY = 0; - try - { - IntPtr dest = XDefaultRootWindow(_mainDisplay); - XTranslateCoordinates(_mainDisplay, _mainWindow, dest, 0, 0, out destX, out destY, out var _); - } - catch - { - } - int num = destX; - SKRect bounds = base.Bounds; - int x = num + (int)((SKRect)(ref bounds)).Left; - int num2 = destY; - bounds = base.Bounds; - int y = num2 + (int)((SKRect)(ref bounds)).Top; - bounds = base.Bounds; - float val; - if (!(((SKRect)(ref bounds)).Width > 10f)) - { - val = 780f; - } - else - { - bounds = base.Bounds; - val = ((SKRect)(ref bounds)).Width; - } - uint width = (uint)Math.Max(100f, val); - bounds = base.Bounds; - float val2; - if (!(((SKRect)(ref bounds)).Height > 10f)) - { - val2 = 300f; - } - else - { - bounds = base.Bounds; - val2 = ((SKRect)(ref bounds)).Height; - } - uint height = (uint)Math.Max(100f, val2); - 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); - } - } - } - - [DllImport("libX11.so.6")] - private static extern IntPtr XInternAtom(IntPtr display, string atomName, bool onlyIfExists); - - [DllImport("libX11.so.6")] - private static extern int XChangeProperty(IntPtr display, IntPtr window, IntPtr property, IntPtr type, int format, int mode, IntPtr data, int nelements); - - [DllImport("libX11.so.6")] - private static extern int XChangeProperty(IntPtr display, IntPtr window, IntPtr property, IntPtr type, int format, int mode, IntPtr[] data, int nelements); - - private void SetWindowAlwaysOnTop(IntPtr window) - { - try - { - IntPtr property = XInternAtom(_mainDisplay, "_NET_WM_STATE", onlyIfExists: false); - IntPtr intPtr = XInternAtom(_mainDisplay, "_NET_WM_STATE_ABOVE", onlyIfExists: false); - IntPtr type = XInternAtom(_mainDisplay, "ATOM", onlyIfExists: false); - IntPtr[] data = new IntPtr[1] { intPtr }; - XChangeProperty(_mainDisplay, window, property, type, 32, 0, data, 1); - } - catch - { - } - } - - private void EnableOverlayMode() - { - if (_gtkWindow == IntPtr.Zero || _useGtk4) - { - return; - } - try - { - gtk3_window_set_type_hint(_gtkWindow, 5); - gtk3_window_set_skip_taskbar_hint(_gtkWindow, setting: true); - gtk3_window_set_skip_pager_hint(_gtkWindow, setting: true); - gtk3_window_set_keep_above(_gtkWindow, setting: true); - gtk3_window_set_decorated(_gtkWindow, decorated: 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() - { - //IL_0039: Unknown result type (might be due to invalid IL or missing references) - //IL_003e: Unknown result type (might be due to invalid IL or missing references) - //IL_004c: Unknown result type (might be due to invalid IL or missing references) - //IL_0051: Unknown result type (might be due to invalid IL or missing references) - //IL_0060: Unknown result type (might be due to invalid IL or missing references) - //IL_0065: Unknown result type (might be due to invalid IL or missing references) - //IL_0079: Unknown result type (might be due to invalid IL or missing references) - //IL_007e: Unknown result type (might be due to invalid IL or missing references) - //IL_0139: Unknown result type (might be due to invalid IL or missing references) - //IL_013e: Unknown result type (might be due to invalid IL or missing references) - if (_mainDisplay == IntPtr.Zero || _mainWindow == IntPtr.Zero) - { - Console.WriteLine("[WebView] Cannot setup embedding - main window not set"); - return; - } - GetWindowPosition(_mainDisplay, _mainWindow, out var x, out var y); - int num = x; - SKRect bounds = base.Bounds; - int num2 = num + (int)((SKRect)(ref bounds)).Left; - int num3 = y; - bounds = base.Bounds; - int num4 = num3 + (int)((SKRect)(ref bounds)).Top; - bounds = base.Bounds; - int num5 = Math.Max(100, (int)((SKRect)(ref bounds)).Width); - bounds = base.Bounds; - int num6 = Math.Max(100, (int)((SKRect)(ref bounds)).Height); - Console.WriteLine($"[WebView] Initial position: ({num2}, {num4}), size ({num5}x{num6})"); - if (!_useGtk4) - { - gtk3_window_move(_gtkWindow, num2, num4); - gtk3_window_resize(_gtkWindow, num5, num6); - } - else - { - gtk4_window_set_default_size(_gtkWindow, num5, num6); - } - _lastBounds = base.Bounds; - } - - private void PositionAtScreenCoordinates() - { - //IL_004f: Unknown result type (might be due to invalid IL or missing references) - //IL_0054: Unknown result type (might be due to invalid IL or missing references) - //IL_0065: Unknown result type (might be due to invalid IL or missing references) - //IL_006a: Unknown result type (might be due to invalid IL or missing references) - //IL_007c: Unknown result type (might be due to invalid IL or missing references) - //IL_0081: Unknown result type (might be due to invalid IL or missing references) - //IL_0095: Unknown result type (might be due to invalid IL or missing references) - //IL_009a: Unknown result type (might be due to invalid IL or missing references) - //IL_0187: Unknown result type (might be due to invalid IL or missing references) - //IL_018c: Unknown result type (might be due to invalid IL or missing references) - //IL_01a9: Unknown result type (might be due to invalid IL or missing references) - //IL_01ae: Unknown result type (might be due to invalid IL or missing references) - //IL_01f6: Unknown result type (might be due to invalid IL or missing references) - //IL_01fb: Unknown result type (might be due to invalid IL or missing references) - if (_gtkWindow == IntPtr.Zero || _mainDisplay == IntPtr.Zero) - { - return; - } - int destX = 0; - int destY = 0; - try - { - IntPtr dest = XDefaultRootWindow(_mainDisplay); - XTranslateCoordinates(_mainDisplay, _mainWindow, dest, 0, 0, out destX, out destY, out var _); - } - catch - { - } - int num = 0; - int num2 = 0; - int num3 = destX; - SKRect bounds = base.Bounds; - int num4 = num3 + (int)((SKRect)(ref bounds)).Left - num; - int num5 = destY; - bounds = base.Bounds; - int num6 = num5 + (int)((SKRect)(ref bounds)).Top - num2; - bounds = base.Bounds; - int num7 = Math.Max(100, (int)((SKRect)(ref bounds)).Width); - bounds = base.Bounds; - int num8 = Math.Max(100, (int)((SKRect)(ref bounds)).Height); - if (Math.Abs(num4 - _lastPosX) > 2 || Math.Abs(num6 - _lastPosY) > 2 || Math.Abs(num7 - _lastWidth) > 2 || Math.Abs(num8 - _lastHeight) > 2) - { - DefaultInterpolatedStringHandler defaultInterpolatedStringHandler = new DefaultInterpolatedStringHandler(57, 8); - defaultInterpolatedStringHandler.AppendLiteral("[WebView] Move to ("); - defaultInterpolatedStringHandler.AppendFormatted(num4); - defaultInterpolatedStringHandler.AppendLiteral(", "); - defaultInterpolatedStringHandler.AppendFormatted(num6); - defaultInterpolatedStringHandler.AppendLiteral("), size ("); - defaultInterpolatedStringHandler.AppendFormatted(num7); - defaultInterpolatedStringHandler.AppendLiteral("x"); - defaultInterpolatedStringHandler.AppendFormatted(num8); - defaultInterpolatedStringHandler.AppendLiteral("), mainWin=("); - defaultInterpolatedStringHandler.AppendFormatted(destX); - defaultInterpolatedStringHandler.AppendLiteral(","); - defaultInterpolatedStringHandler.AppendFormatted(destY); - defaultInterpolatedStringHandler.AppendLiteral("), bounds=("); - bounds = base.Bounds; - defaultInterpolatedStringHandler.AppendFormatted(((SKRect)(ref bounds)).Left); - defaultInterpolatedStringHandler.AppendLiteral(","); - bounds = base.Bounds; - defaultInterpolatedStringHandler.AppendFormatted(((SKRect)(ref bounds)).Top); - defaultInterpolatedStringHandler.AppendLiteral(")"); - Console.WriteLine(defaultInterpolatedStringHandler.ToStringAndClear()); - _lastPosX = num4; - _lastPosY = num6; - _lastWidth = num7; - _lastHeight = num8; - _lastBounds = base.Bounds; - } - if (!_useGtk4) - { - gtk3_window_move(_gtkWindow, num4, num6); - gtk3_window_resize(_gtkWindow, num7, num8); - IntPtr intPtr = gtk3_widget_get_window(_gtkWindow); - if (intPtr != IntPtr.Zero) - { - IntPtr intPtr2 = gdk3_x11_window_get_xid(intPtr); - if (intPtr2 != IntPtr.Zero) - { - XRaiseWindow(_mainDisplay, intPtr2); - XFlush(_mainDisplay); - } - } - while (gtk3_events_pending()) - { - gtk3_main_iteration_do(blocking: false); - } - } - else - { - gtk4_window_set_default_size(_gtkWindow, num7, num8); - } - } - - [DllImport("libX11.so.6")] - private static extern int XMapRaised(IntPtr display, IntPtr window); - - 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); - } - IntPtr result = IntPtr.Zero; - if (_useGtk4) - { - IntPtr intPtr = gtk4_native_get_surface(_gtkWindow); - if (intPtr != IntPtr.Zero) - { - try - { - result = gdk4_x11_surface_get_xid(intPtr); - } - catch - { - } - } - } - else - { - IntPtr intPtr2 = gtk3_widget_get_window(_gtkWindow); - if (intPtr2 != IntPtr.Zero) - { - try - { - result = gdk3_x11_window_get_xid(intPtr2); - } - catch - { - } - } - } - return result; - } - - [DllImport("libX11.so.6")] - private static extern int XGetWindowAttributes(IntPtr display, IntPtr window, out XWindowAttributes attributes); - - [DllImport("libX11.so.6")] - 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.so.6")] - private static extern IntPtr XDefaultRootWindow(IntPtr display); - - private void GetWindowPosition(IntPtr display, IntPtr window, out int x, out int y) - { - x = 0; - y = 0; - try - { - IntPtr dest = XDefaultRootWindow(display); - if (XTranslateCoordinates(display, window, dest, 0, 0, out x, out y, out var _)) - { - 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() - { - //IL_000e: Unknown result type (might be due to invalid IL or missing references) - //IL_0013: Unknown result type (might be due to invalid IL or missing references) - //IL_0024: Unknown result type (might be due to invalid IL or missing references) - //IL_0029: Unknown result type (might be due to invalid IL or missing references) - //IL_003b: Unknown result type (might be due to invalid IL or missing references) - //IL_0040: Unknown result type (might be due to invalid IL or missing references) - //IL_0062: Unknown result type (might be due to invalid IL or missing references) - //IL_0067: Unknown result type (might be due to invalid IL or missing references) - //IL_0089: Unknown result type (might be due to invalid IL or missing references) - //IL_008e: Unknown result type (might be due to invalid IL or missing references) - //IL_00de: Unknown result type (might be due to invalid IL or missing references) - //IL_00e3: Unknown result type (might be due to invalid IL or missing references) - //IL_00e9: Unknown result type (might be due to invalid IL or missing references) - //IL_00ee: Unknown result type (might be due to invalid IL or missing references) - //IL_00fa: Unknown result type (might be due to invalid IL or missing references) - //IL_00ff: Unknown result type (might be due to invalid IL or missing references) - //IL_0110: Unknown result type (might be due to invalid IL or missing references) - //IL_0115: Unknown result type (might be due to invalid IL or missing references) - //IL_012b: Unknown result type (might be due to invalid IL or missing references) - //IL_0130: Unknown result type (might be due to invalid IL or missing references) - //IL_00b0: Unknown result type (might be due to invalid IL or missing references) - //IL_00b5: Unknown result type (might be due to invalid IL or missing references) - if (_mainDisplay == IntPtr.Zero) - { - return; - } - SKRect bounds = base.Bounds; - if (((SKRect)(ref bounds)).Width < 10f) - { - return; - } - bounds = base.Bounds; - if (((SKRect)(ref bounds)).Height < 10f) - { - return; - } - bounds = base.Bounds; - if (!(Math.Abs(((SKRect)(ref bounds)).Left - ((SKRect)(ref _lastBounds)).Left) > 1f)) - { - bounds = base.Bounds; - if (!(Math.Abs(((SKRect)(ref bounds)).Top - ((SKRect)(ref _lastBounds)).Top) > 1f)) - { - bounds = base.Bounds; - if (!(Math.Abs(((SKRect)(ref bounds)).Width - ((SKRect)(ref _lastBounds)).Width) > 1f)) - { - bounds = base.Bounds; - if (!(Math.Abs(((SKRect)(ref bounds)).Height - ((SKRect)(ref _lastBounds)).Height) > 1f)) - { - return; - } - } - } - } - _lastBounds = base.Bounds; - bounds = base.Bounds; - int num = (int)((SKRect)(ref bounds)).Left; - bounds = base.Bounds; - int num2 = (int)((SKRect)(ref bounds)).Top; - bounds = base.Bounds; - uint num3 = (uint)Math.Max(10f, ((SKRect)(ref bounds)).Width); - bounds = base.Bounds; - uint num4 = (uint)Math.Max(10f, ((SKRect)(ref bounds)).Height); - if (_isProperlyReparented && _gtkX11Window != IntPtr.Zero) - { - Console.WriteLine($"[WebView] UpdateEmbedded (reparented): ({num}, {num2}), size ({num3}x{num4})"); - XMoveResizeWindow(_mainDisplay, _gtkX11Window, num, num2, num3, num4); - XFlush(_mainDisplay); - } - else if (_x11Container != IntPtr.Zero) - { - Console.WriteLine($"[WebView] UpdateEmbedded (container): ({num}, {num2}), size ({num3}x{num4})"); - XMoveResizeWindow(_mainDisplay, _x11Container, num, num2, num3, num4); - if (_gtkX11Window != IntPtr.Zero && _isProperlyReparented) - { - XMoveResizeWindow(_mainDisplay, _gtkX11Window, 0, 0, num3, num4); - } - else if (_gtkWindow != IntPtr.Zero) - { - PositionAtScreenCoordinates(); - } - XFlush(_mainDisplay); - } - else if (_gtkWindow != IntPtr.Zero) - { - PositionAtScreenCoordinates(); - } - } - - public void HideNativeWindow() - { - if (_gtkWindow != IntPtr.Zero) - { - if (_useGtk4) - { - gtk4_widget_hide(_gtkWindow); - } - else - { - gtk3_widget_hide(_gtkWindow); - } - } - } - - protected override void OnDraw(SKCanvas canvas, SKRect bounds) - { - //IL_0002: Unknown result type (might be due to invalid IL or missing references) - //IL_0009: Unknown result type (might be due to invalid IL or missing references) - //IL_0134: Unknown result type (might be due to invalid IL or missing references) - //IL_0139: Unknown result type (might be due to invalid IL or missing references) - //IL_013b: Unknown result type (might be due to invalid IL or missing references) - //IL_0145: Unknown result type (might be due to invalid IL or missing references) - //IL_014d: Expected O, but got Unknown - //IL_014e: Unknown result type (might be due to invalid IL or missing references) - //IL_0155: Unknown result type (might be due to invalid IL or missing references) - //IL_015a: Unknown result type (might be due to invalid IL or missing references) - //IL_016a: Unknown result type (might be due to invalid IL or missing references) - //IL_0174: Unknown result type (might be due to invalid IL or missing references) - //IL_017b: Unknown result type (might be due to invalid IL or missing references) - //IL_0187: Expected O, but got Unknown - //IL_0188: Unknown result type (might be due to invalid IL or missing references) - //IL_019f: Unknown result type (might be due to invalid IL or missing references) - //IL_01a4: Unknown result type (might be due to invalid IL or missing references) - //IL_01ab: Unknown result type (might be due to invalid IL or missing references) - //IL_01b5: Unknown result type (might be due to invalid IL or missing references) - //IL_01bc: Unknown result type (might be due to invalid IL or missing references) - //IL_01c7: Unknown result type (might be due to invalid IL or missing references) - //IL_01d0: Expected O, but got Unknown - //IL_0226: Unknown result type (might be due to invalid IL or missing references) - //IL_023d: Unknown result type (might be due to invalid IL or missing references) - //IL_0242: Unknown result type (might be due to invalid IL or missing references) - //IL_0249: Unknown result type (might be due to invalid IL or missing references) - //IL_0253: Unknown result type (might be due to invalid IL or missing references) - //IL_025a: Unknown result type (might be due to invalid IL or missing references) - //IL_0267: Expected O, but got Unknown - //IL_0346: Unknown result type (might be due to invalid IL or missing references) - //IL_034b: Unknown result type (might be due to invalid IL or missing references) - //IL_0352: Unknown result type (might be due to invalid IL or missing references) - //IL_035c: Unknown result type (might be due to invalid IL or missing references) - //IL_0363: Unknown result type (might be due to invalid IL or missing references) - //IL_0370: Expected O, but got Unknown - //IL_040e: Unknown result type (might be due to invalid IL or missing references) - //IL_0413: Unknown result type (might be due to invalid IL or missing references) - //IL_0423: Unknown result type (might be due to invalid IL or missing references) - //IL_042d: Unknown result type (might be due to invalid IL or missing references) - //IL_0436: Expected O, but got Unknown - //IL_0118: Unknown result type (might be due to invalid IL or missing references) - //IL_0119: Unknown result type (might be due to invalid IL or missing references) - //IL_0437: Unknown result type (might be due to invalid IL or missing references) - //IL_043e: Unknown result type (might be due to invalid IL or missing references) - //IL_044a: Expected O, but got Unknown - //IL_047a: Unknown result type (might be due to invalid IL or missing references) - //IL_047f: Unknown result type (might be due to invalid IL or missing references) - //IL_0481: Unknown result type (might be due to invalid IL or missing references) - //IL_0486: Unknown result type (might be due to invalid IL or missing references) - //IL_0493: Unknown result type (might be due to invalid IL or missing references) - //IL_049d: Unknown result type (might be due to invalid IL or missing references) - //IL_04a6: Expected O, but got Unknown - //IL_04a7: Unknown result type (might be due to invalid IL or missing references) - //IL_04ae: Unknown result type (might be due to invalid IL or missing references) - //IL_04ba: Expected O, but got Unknown - base.OnDraw(canvas, bounds); - base.Bounds = bounds; - if (_isInitialized) - { - while (gtk3_events_pending()) - { - gtk3_main_iteration_do(blocking: false); - } - if (_gtkWindow != IntPtr.Zero && _mainDisplay != IntPtr.Zero) - { - bool flag = Math.Abs(((SKRect)(ref bounds)).Left - ((SKRect)(ref _lastBounds)).Left) > 1f || Math.Abs(((SKRect)(ref bounds)).Top - ((SKRect)(ref _lastBounds)).Top) > 1f || Math.Abs(((SKRect)(ref bounds)).Width - ((SKRect)(ref _lastBounds)).Width) > 1f || Math.Abs(((SKRect)(ref bounds)).Height - ((SKRect)(ref _lastBounds)).Height) > 1f; - if (!flag && ((SKRect)(ref _lastBounds)).Width < 150f && ((SKRect)(ref bounds)).Width > 150f) - { - flag = true; - } - if (flag && ((SKRect)(ref bounds)).Width > 50f && ((SKRect)(ref bounds)).Height > 50f) - { - PositionUsingGtk(); - _lastBounds = bounds; - } - } - } - if (_isInitialized && _gtkWindow != IntPtr.Zero) - { - return; - } - SKPaint val = new SKPaint - { - Color = base.BackgroundColor, - Style = (SKPaintStyle)0 - }; - try - { - canvas.DrawRect(bounds, val); - SKPaint val2 = new SKPaint - { - Color = new SKColor((byte)200, (byte)200, (byte)200), - Style = (SKPaintStyle)1, - StrokeWidth = 1f - }; - try - { - canvas.DrawRect(bounds, val2); - float midX = ((SKRect)(ref bounds)).MidX; - float midY = ((SKRect)(ref bounds)).MidY; - SKPaint val3 = new SKPaint - { - Color = new SKColor((byte)100, (byte)100, (byte)100), - Style = (SKPaintStyle)1, - StrokeWidth = 2f, - IsAntialias = true - }; - try - { - canvas.DrawCircle(midX, midY - 20f, 25f, val3); - canvas.DrawLine(midX - 25f, midY - 20f, midX + 25f, midY - 20f, val3); - canvas.DrawArc(new SKRect(midX - 15f, midY - 45f, midX + 15f, midY + 5f), 0f, 180f, false, val3); - SKPaint val4 = new SKPaint - { - Color = new SKColor((byte)80, (byte)80, (byte)80), - IsAntialias = true, - TextSize = 14f - }; - try - { - string text; - if (!IsSupported) - { - text = "WebKitGTK not installed"; - } - else if (_isInitialized) - { - text = (string.IsNullOrEmpty(_source) ? "No URL loaded" : ("Loading: " + _source)); - if (_loadProgress > 0.0 && _loadProgress < 1.0) - { - text = $"Loading: {(int)(_loadProgress * 100.0)}%"; - } - } - else - { - text = "WebView (click to open)"; - } - float num = val4.MeasureText(text); - canvas.DrawText(text, midX - num / 2f, midY + 30f, val4); - if (!IsSupported) - { - SKPaint val5 = new SKPaint - { - Color = new SKColor((byte)120, (byte)120, (byte)120), - IsAntialias = true, - TextSize = 11f - }; - try - { - string text2 = "Install: sudo apt install libwebkit2gtk-4.1-0"; - float num2 = val5.MeasureText(text2); - canvas.DrawText(text2, midX - num2 / 2f, midY + 50f, val5); - } - finally - { - ((IDisposable)val5)?.Dispose(); - } - } - if (!(_loadProgress > 0.0) || !(_loadProgress < 1.0)) - { - return; - } - SKRect val6 = default(SKRect); - ((SKRect)(ref val6))._002Ector(((SKRect)(ref bounds)).Left + 20f, ((SKRect)(ref bounds)).Bottom - 30f, ((SKRect)(ref bounds)).Right - 20f, ((SKRect)(ref bounds)).Bottom - 20f); - SKPaint val7 = new SKPaint - { - Color = new SKColor((byte)230, (byte)230, (byte)230), - Style = (SKPaintStyle)0 - }; - try - { - canvas.DrawRoundRect(new SKRoundRect(val6, 5f), val7); - float num3 = ((SKRect)(ref val6)).Width * (float)_loadProgress; - SKRect val8 = new SKRect(((SKRect)(ref val6)).Left, ((SKRect)(ref val6)).Top, ((SKRect)(ref val6)).Left + num3, ((SKRect)(ref val6)).Bottom); - SKPaint val9 = new SKPaint - { - Color = new SKColor((byte)33, (byte)150, (byte)243), - Style = (SKPaintStyle)0 - }; - try - { - canvas.DrawRoundRect(new SKRoundRect(val8, 5f), val9); - } - finally - { - ((IDisposable)val9)?.Dispose(); - } - } - finally - { - ((IDisposable)val7)?.Dispose(); - } - } - finally - { - ((IDisposable)val4)?.Dispose(); - } - } - finally - { - ((IDisposable)val3)?.Dispose(); - } - } - finally - { - ((IDisposable)val2)?.Dispose(); - } - } - finally - { - ((IDisposable)val)?.Dispose(); - } - } - - public override void OnPointerPressed(PointerEventArgs e) - { - base.OnPointerPressed(e); - if (!_isInitialized && IsSupported) - { - Initialize(); - ShowNativeWindow(); - } - else if (_isInitialized) - { - ShowNativeWindow(); - } - } - - protected override void Dispose(bool disposing) - { - if (disposing) - { - lock (_activeWebViews) - { - _activeWebViews.Remove(this); - } - if (_webView != IntPtr.Zero) - { - _webViewInstances.Remove(_webView); - } - if (_gtkWindow != IntPtr.Zero) - { - if (_useGtk4) - { - gtk4_widget_hide(_gtkWindow); - } - else - { - gtk3_widget_hide(_gtkWindow); - } - 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; - } - base.Dispose(disposing); - } - - public static void ProcessGtkEvents() - { - bool flag; - lock (_activeWebViews) - { - flag = _activeWebViews.Count > 0; - } - if (flag && _gtkInitialized) - { - while (g_main_context_iteration(IntPtr.Zero, mayBlock: false)) - { - } - } - } + [DllImport(LibGtk4, EntryPoint = "gtk_widget_show")] + private static extern void gtk4_widget_show(IntPtr widget); + + [DllImport(LibGtk3, EntryPoint = "gtk_widget_show_all")] + private static extern void gtk3_widget_show_all(IntPtr widget); + + [DllImport(LibGtk4, EntryPoint = "gtk_widget_hide")] + private static extern void gtk4_widget_hide(IntPtr widget); + + [DllImport(LibGtk3, EntryPoint = "gtk_widget_hide")] + private static extern void gtk3_widget_hide(IntPtr widget); + + [DllImport(LibGtk4, EntryPoint = "gtk_widget_get_width")] + private static extern int gtk4_widget_get_width(IntPtr widget); + + [DllImport(LibGtk4, EntryPoint = "gtk_widget_get_height")] + private static extern int gtk4_widget_get_height(IntPtr widget); + + // GObject + [DllImport(LibGObject, EntryPoint = "g_object_unref")] + private static extern void g_object_unref(IntPtr obj); + + [DllImport(LibGObject, EntryPoint = "g_signal_connect_data")] + private static extern ulong g_signal_connect_data(IntPtr instance, + [MarshalAs(UnmanagedType.LPStr)] string signal, + IntPtr handler, IntPtr data, IntPtr destroyData, int flags); + + // GLib main loop (for event processing) + [DllImport(LibGLib, EntryPoint = "g_main_context_iteration")] + private static extern bool g_main_context_iteration(IntPtr context, bool mayBlock); + + #endregion + + #region WebKit Functions + + // We'll load these dynamically based on available version + private delegate IntPtr WebKitWebViewNewDelegate(); + private delegate void WebKitWebViewLoadUriDelegate(IntPtr webView, [MarshalAs(UnmanagedType.LPStr)] string uri); + private delegate void WebKitWebViewLoadHtmlDelegate(IntPtr webView, [MarshalAs(UnmanagedType.LPStr)] string html, [MarshalAs(UnmanagedType.LPStr)] string? baseUri); + private delegate IntPtr WebKitWebViewGetUriDelegate(IntPtr webView); + private delegate IntPtr WebKitWebViewGetTitleDelegate(IntPtr webView); + private delegate void WebKitWebViewGoBackDelegate(IntPtr webView); + private delegate void WebKitWebViewGoForwardDelegate(IntPtr webView); + private delegate bool WebKitWebViewCanGoBackDelegate(IntPtr webView); + private delegate bool WebKitWebViewCanGoForwardDelegate(IntPtr webView); + private delegate void WebKitWebViewReloadDelegate(IntPtr webView); + private delegate void WebKitWebViewStopLoadingDelegate(IntPtr webView); + private delegate double WebKitWebViewGetEstimatedLoadProgressDelegate(IntPtr webView); + private delegate IntPtr WebKitWebViewGetSettingsDelegate(IntPtr webView); + private delegate void WebKitSettingsSetEnableJavascriptDelegate(IntPtr settings, bool enabled); + + private static WebKitWebViewNewDelegate? _webkitWebViewNew; + private static WebKitWebViewLoadUriDelegate? _webkitLoadUri; + private static WebKitWebViewLoadHtmlDelegate? _webkitLoadHtml; + private static WebKitWebViewGetUriDelegate? _webkitGetUri; + private static WebKitWebViewGetTitleDelegate? _webkitGetTitle; + private static WebKitWebViewGoBackDelegate? _webkitGoBack; + private static WebKitWebViewGoForwardDelegate? _webkitGoForward; + private static WebKitWebViewCanGoBackDelegate? _webkitCanGoBack; + private static WebKitWebViewCanGoForwardDelegate? _webkitCanGoForward; + private static WebKitWebViewReloadDelegate? _webkitReload; + private static WebKitWebViewStopLoadingDelegate? _webkitStopLoading; + private static WebKitWebViewGetEstimatedLoadProgressDelegate? _webkitGetProgress; + private static WebKitWebViewGetSettingsDelegate? _webkitGetSettings; + private static WebKitSettingsSetEnableJavascriptDelegate? _webkitSetJavascript; + + [DllImport("libdl.so.2")] + private static extern IntPtr dlopen([MarshalAs(UnmanagedType.LPStr)] string? filename, int flags); + + [DllImport("libdl.so.2")] + private static extern IntPtr dlsym(IntPtr handle, [MarshalAs(UnmanagedType.LPStr)] string symbol); + + [DllImport("libdl.so.2")] + private static extern IntPtr dlerror(); + + private const int RTLD_NOW = 2; + private const int RTLD_GLOBAL = 0x100; + + private static IntPtr _webkitHandle; + + #endregion + + #region Fields + + private IntPtr _gtkWindow; + private IntPtr _webView; + private string _source = ""; + private string _html = ""; + private bool _isInitialized; + private bool _javascriptEnabled = true; + private double _loadProgress; + + #endregion + + #region Properties + + /// + /// Gets or sets the URL to navigate to. + /// + public string Source + { + get => _source; + set + { + if (_source != value) + { + _source = value; + if (_isInitialized && !string.IsNullOrEmpty(value)) + { + LoadUrl(value); + } + Invalidate(); + } + } + } + + /// + /// Gets or sets the HTML content to display. + /// + public string Html + { + get => _html; + set + { + if (_html != value) + { + _html = value; + if (_isInitialized && !string.IsNullOrEmpty(value)) + { + LoadHtml(value); + } + Invalidate(); + } + } + } + + /// + /// Gets whether the WebView can navigate back. + /// + public bool CanGoBack => _webView != IntPtr.Zero && _webkitCanGoBack?.Invoke(_webView) == true; + + /// + /// Gets whether the WebView can navigate forward. + /// + public bool CanGoForward => _webView != IntPtr.Zero && _webkitCanGoForward?.Invoke(_webView) == true; + + /// + /// Gets the current URL. + /// + public string? CurrentUrl + { + get + { + if (_webView == IntPtr.Zero || _webkitGetUri == null) return null; + var ptr = _webkitGetUri(_webView); + return ptr != IntPtr.Zero ? Marshal.PtrToStringAnsi(ptr) : null; + } + } + + /// + /// Gets the current page title. + /// + public string? Title + { + get + { + if (_webView == IntPtr.Zero || _webkitGetTitle == null) return null; + var ptr = _webkitGetTitle(_webView); + return ptr != IntPtr.Zero ? Marshal.PtrToStringAnsi(ptr) : null; + } + } + + /// + /// Gets or sets whether JavaScript is enabled. + /// + public bool JavaScriptEnabled + { + get => _javascriptEnabled; + set + { + _javascriptEnabled = value; + UpdateJavaScriptSetting(); + } + } + + /// + /// Gets the load progress (0.0 to 1.0). + /// + public double LoadProgress => _loadProgress; + + /// + /// Gets whether WebKit is available on this system. + /// + public static bool IsSupported => InitializeWebKit(); + + #endregion + + #region Events + + public event EventHandler? Navigating; + public event EventHandler? Navigated; + public event EventHandler? TitleChanged; + public event EventHandler? LoadProgressChanged; + + #endregion + + #region Constructor + + public SkiaWebView() + { + RequestedWidth = 400; + RequestedHeight = 300; + BackgroundColor = SKColors.White; + } + + #endregion + + #region Initialization + + private static bool InitializeWebKit() + { + if (_webkitHandle != IntPtr.Zero) return true; + + // Try WebKitGTK 6.0 (GTK4) first + _webkitHandle = dlopen(LibWebKit2Gtk4, RTLD_NOW | RTLD_GLOBAL); + if (_webkitHandle != IntPtr.Zero) + { + _useGtk4 = true; + _webkitLib = LibWebKit2Gtk4; + } + else + { + // Fall back to WebKitGTK 4.1 (GTK3) + _webkitHandle = dlopen(LibWebKit2Gtk3, RTLD_NOW | RTLD_GLOBAL); + if (_webkitHandle != IntPtr.Zero) + { + _useGtk4 = false; + _webkitLib = LibWebKit2Gtk3; + } + else + { + // Try older WebKitGTK 4.0 + _webkitHandle = dlopen("libwebkit2gtk-4.0.so.37", RTLD_NOW | RTLD_GLOBAL); + if (_webkitHandle != IntPtr.Zero) + { + _useGtk4 = false; + _webkitLib = "libwebkit2gtk-4.0.so.37"; + } + } + } + + if (_webkitHandle == IntPtr.Zero) + { + Console.WriteLine("[WebView] WebKitGTK not found. Install with: sudo apt install libwebkit2gtk-4.1-0"); + return false; + } + + // Load function pointers + _webkitWebViewNew = LoadFunction("webkit_web_view_new"); + _webkitLoadUri = LoadFunction("webkit_web_view_load_uri"); + _webkitLoadHtml = LoadFunction("webkit_web_view_load_html"); + _webkitGetUri = LoadFunction("webkit_web_view_get_uri"); + _webkitGetTitle = LoadFunction("webkit_web_view_get_title"); + _webkitGoBack = LoadFunction("webkit_web_view_go_back"); + _webkitGoForward = LoadFunction("webkit_web_view_go_forward"); + _webkitCanGoBack = LoadFunction("webkit_web_view_can_go_back"); + _webkitCanGoForward = LoadFunction("webkit_web_view_can_go_forward"); + _webkitReload = LoadFunction("webkit_web_view_reload"); + _webkitStopLoading = LoadFunction("webkit_web_view_stop_loading"); + _webkitGetProgress = LoadFunction("webkit_web_view_get_estimated_load_progress"); + _webkitGetSettings = LoadFunction("webkit_web_view_get_settings"); + _webkitSetJavascript = LoadFunction("webkit_settings_set_enable_javascript"); + + Console.WriteLine($"[WebView] Using {_webkitLib}"); + return _webkitWebViewNew != null; + } + + private static T? LoadFunction(string name) where T : Delegate + { + var ptr = dlsym(_webkitHandle, name); + if (ptr == IntPtr.Zero) return null; + return Marshal.GetDelegateForFunctionPointer(ptr); + } + + private void Initialize() + { + if (_isInitialized) return; + if (!InitializeWebKit()) return; + + try + { + // Initialize GTK if needed + if (!_gtkInitialized) + { + if (_useGtk4) + { + gtk4_init(); + } + else + { + int argc = 0; + IntPtr argv = IntPtr.Zero; + gtk3_init_check(ref argc, ref argv); + } + _gtkInitialized = true; + } + + // Create WebKit view + _webView = _webkitWebViewNew!(); + if (_webView == IntPtr.Zero) + { + Console.WriteLine("[WebView] Failed to create WebKit view"); + return; + } + + // Create GTK window to host the WebView + if (_useGtk4) + { + _gtkWindow = gtk4_window_new(); + gtk4_window_set_default_size(_gtkWindow, (int)RequestedWidth, (int)RequestedHeight); + gtk4_window_set_child(_gtkWindow, _webView); + } + else + { + _gtkWindow = gtk3_window_new(0); // GTK_WINDOW_TOPLEVEL + gtk3_window_set_default_size(_gtkWindow, (int)RequestedWidth, (int)RequestedHeight); + gtk3_container_add(_gtkWindow, _webView); + } + + UpdateJavaScriptSetting(); + _isInitialized = true; + + // Load initial content + if (!string.IsNullOrEmpty(_source)) + { + LoadUrl(_source); + } + else if (!string.IsNullOrEmpty(_html)) + { + LoadHtml(_html); + } + + Console.WriteLine("[WebView] Initialized successfully"); + } + catch (Exception ex) + { + Console.WriteLine($"[WebView] Initialization failed: {ex.Message}"); + } + } + + #endregion + + #region Navigation + + public void LoadUrl(string url) + { + if (!_isInitialized) Initialize(); + if (_webView == IntPtr.Zero || _webkitLoadUri == null) return; + + Navigating?.Invoke(this, new WebNavigatingEventArgs(url)); + _webkitLoadUri(_webView, url); + } + + public void LoadHtml(string html, string? baseUrl = null) + { + if (!_isInitialized) Initialize(); + if (_webView == IntPtr.Zero || _webkitLoadHtml == null) return; + + _webkitLoadHtml(_webView, html, baseUrl); + } + + public void GoBack() + { + if (_webView != IntPtr.Zero && CanGoBack) + { + _webkitGoBack?.Invoke(_webView); + } + } + + public void GoForward() + { + if (_webView != IntPtr.Zero && CanGoForward) + { + _webkitGoForward?.Invoke(_webView); + } + } + + public void Reload() + { + if (_webView != IntPtr.Zero) + { + _webkitReload?.Invoke(_webView); + } + } + + public void Stop() + { + if (_webView != IntPtr.Zero) + { + _webkitStopLoading?.Invoke(_webView); + } + } + + private void UpdateJavaScriptSetting() + { + if (_webView == IntPtr.Zero || _webkitGetSettings == null || _webkitSetJavascript == null) return; + + var settings = _webkitGetSettings(_webView); + if (settings != IntPtr.Zero) + { + _webkitSetJavascript(settings, _javascriptEnabled); + } + } + + #endregion + + #region Event Processing + + /// + /// Process pending GTK events. Call this from your main loop. + /// + public void ProcessEvents() + { + if (!_isInitialized) return; + + // Process GTK events + g_main_context_iteration(IntPtr.Zero, false); + + // Update progress + if (_webView != IntPtr.Zero && _webkitGetProgress != null) + { + var progress = _webkitGetProgress(_webView); + if (Math.Abs(progress - _loadProgress) > 0.01) + { + _loadProgress = progress; + LoadProgressChanged?.Invoke(this, progress); + } + } + } + + /// + /// Show the native WebView window (for testing/debugging). + /// + public void ShowNativeWindow() + { + if (!_isInitialized) Initialize(); + if (_gtkWindow == IntPtr.Zero) return; + + if (_useGtk4) + { + gtk4_widget_show(_gtkWindow); + } + else + { + gtk3_widget_show_all(_gtkWindow); + } + } + + /// + /// Hide the native WebView window. + /// + public void HideNativeWindow() + { + if (_gtkWindow == IntPtr.Zero) return; + + if (_useGtk4) + { + gtk4_widget_hide(_gtkWindow); + } + else + { + gtk3_widget_hide(_gtkWindow); + } + } + + #endregion + + #region Rendering + + protected override void OnDraw(SKCanvas canvas, SKRect bounds) + { + base.OnDraw(canvas, bounds); + + // Draw placeholder/loading state + using var bgPaint = new SKPaint { Color = BackgroundColor, Style = SKPaintStyle.Fill }; + canvas.DrawRect(bounds, bgPaint); + + // Draw border + using var borderPaint = new SKPaint + { + Color = new SKColor(200, 200, 200), + Style = SKPaintStyle.Stroke, + StrokeWidth = 1 + }; + canvas.DrawRect(bounds, borderPaint); + + // Draw web icon and status + var centerX = bounds.MidX; + var centerY = bounds.MidY; + + // Globe icon + using var iconPaint = new SKPaint + { + Color = new SKColor(100, 100, 100), + Style = SKPaintStyle.Stroke, + StrokeWidth = 2, + IsAntialias = true + }; + canvas.DrawCircle(centerX, centerY - 20, 25, iconPaint); + canvas.DrawLine(centerX - 25, centerY - 20, centerX + 25, centerY - 20, iconPaint); + canvas.DrawArc(new SKRect(centerX - 15, centerY - 45, centerX + 15, centerY + 5), 0, 180, false, iconPaint); + + // Status text + using var textPaint = new SKPaint + { + Color = new SKColor(80, 80, 80), + IsAntialias = true, + TextSize = 14 + }; + + string statusText; + if (!IsSupported) + { + statusText = "WebKitGTK not installed"; + } + else if (_isInitialized) + { + statusText = string.IsNullOrEmpty(_source) ? "No URL loaded" : $"Loading: {_source}"; + if (_loadProgress > 0 && _loadProgress < 1) + { + statusText = $"Loading: {(int)(_loadProgress * 100)}%"; + } + } + else + { + statusText = "WebView (click to open)"; + } + + var textWidth = textPaint.MeasureText(statusText); + canvas.DrawText(statusText, centerX - textWidth / 2, centerY + 30, textPaint); + + // Draw install hint if not supported + if (!IsSupported) + { + using var hintPaint = new SKPaint + { + Color = new SKColor(120, 120, 120), + IsAntialias = true, + TextSize = 11 + }; + var hint = "Install: sudo apt install libwebkit2gtk-4.1-0"; + var hintWidth = hintPaint.MeasureText(hint); + canvas.DrawText(hint, centerX - hintWidth / 2, centerY + 50, hintPaint); + } + + // Progress bar + if (_loadProgress > 0 && _loadProgress < 1) + { + var progressRect = new SKRect(bounds.Left + 20, bounds.Bottom - 30, bounds.Right - 20, bounds.Bottom - 20); + using var progressBgPaint = new SKPaint { Color = new SKColor(230, 230, 230), Style = SKPaintStyle.Fill }; + canvas.DrawRoundRect(new SKRoundRect(progressRect, 5), progressBgPaint); + + var filledWidth = progressRect.Width * (float)_loadProgress; + var filledRect = new SKRect(progressRect.Left, progressRect.Top, progressRect.Left + filledWidth, progressRect.Bottom); + using var progressPaint = new SKPaint { Color = new SKColor(33, 150, 243), Style = SKPaintStyle.Fill }; + canvas.DrawRoundRect(new SKRoundRect(filledRect, 5), progressPaint); + } + } + + public override void OnPointerPressed(PointerEventArgs e) + { + base.OnPointerPressed(e); + + if (!_isInitialized && IsSupported) + { + Initialize(); + ShowNativeWindow(); + } + else if (_isInitialized) + { + ShowNativeWindow(); + } + } + + #endregion + + #region Cleanup + + protected override void Dispose(bool disposing) + { + if (disposing) + { + if (_gtkWindow != IntPtr.Zero) + { + if (_useGtk4) + { + gtk4_widget_hide(_gtkWindow); + } + else + { + gtk3_widget_hide(_gtkWindow); + } + g_object_unref(_gtkWindow); + _gtkWindow = IntPtr.Zero; + } + _webView = IntPtr.Zero; + _isInitialized = false; + } + + base.Dispose(disposing); + } + + #endregion } + +#region Event Args + +public class WebNavigatingEventArgs : EventArgs +{ + public string Url { get; } + public bool Cancel { get; set; } + + public WebNavigatingEventArgs(string url) + { + Url = url; + } +} + +public class WebNavigatedEventArgs : EventArgs +{ + public string Url { get; } + public bool Success { get; } + public string? Error { get; } + + public WebNavigatedEventArgs(string url, bool success, string? error = null) + { + Url = url; + Success = success; + Error = error; + } +} + +#endregion diff --git a/Window/CursorType.cs b/Window/CursorType.cs deleted file mode 100644 index 6bf51ae..0000000 --- a/Window/CursorType.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Microsoft.Maui.Platform.Linux.Window; - -public enum CursorType -{ - Arrow, - Hand, - Text -} diff --git a/Window/GtkHostWindow.cs b/Window/GtkHostWindow.cs deleted file mode 100644 index 3381729..0000000 --- a/Window/GtkHostWindow.cs +++ /dev/null @@ -1,345 +0,0 @@ -using System; -using System.IO; -using System.Runtime.InteropServices; -using Microsoft.Maui.Platform.Linux.Native; -using Microsoft.Maui.Platform.Linux.Rendering; - -namespace Microsoft.Maui.Platform.Linux.Window; - -public sealed class GtkHostWindow : IDisposable -{ - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - private delegate bool DeleteEventDelegate(IntPtr widget, IntPtr eventData, IntPtr userData); - - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - private delegate bool ConfigureEventDelegate(IntPtr widget, IntPtr eventData, IntPtr userData); - - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - private delegate bool ButtonEventDelegate(IntPtr widget, IntPtr eventData, IntPtr userData); - - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - private delegate bool MotionEventDelegate(IntPtr widget, IntPtr eventData, IntPtr userData); - - [StructLayout(LayoutKind.Explicit)] - private struct GdkEventButton - { - [FieldOffset(0)] - public int type; - - [FieldOffset(8)] - public IntPtr window; - - [FieldOffset(16)] - public sbyte send_event; - - [FieldOffset(20)] - public uint time; - - [FieldOffset(24)] - public double x; - - [FieldOffset(32)] - public double y; - - [FieldOffset(40)] - public IntPtr axes; - - [FieldOffset(48)] - public uint state; - - [FieldOffset(52)] - public uint button; - } - - [StructLayout(LayoutKind.Explicit)] - private struct GdkEventMotion - { - [FieldOffset(0)] - public int type; - - [FieldOffset(8)] - public IntPtr window; - - [FieldOffset(16)] - public sbyte send_event; - - [FieldOffset(20)] - public uint time; - - [FieldOffset(24)] - public double x; - - [FieldOffset(32)] - public double y; - } - - private IntPtr _window; - - private IntPtr _overlay; - - private IntPtr _webViewLayer; - - private GtkSkiaSurfaceWidget? _skiaSurface; - - private bool _disposed; - - private bool _isRunning; - - private int _width; - - private int _height; - - private readonly DeleteEventDelegate _deleteEventHandler; - - private readonly ConfigureEventDelegate _configureEventHandler; - - private readonly ButtonEventDelegate _buttonPressHandler; - - private readonly ButtonEventDelegate _buttonReleaseHandler; - - private readonly MotionEventDelegate _motionHandler; - - public IntPtr Window => _window; - - public IntPtr Overlay => _overlay; - - public IntPtr WebViewLayer => _webViewLayer; - - public GtkSkiaSurfaceWidget? SkiaSurface => _skiaSurface; - - public int Width => _width; - - public int Height => _height; - - public bool IsRunning => _isRunning; - - public event EventHandler<(int Width, int Height)>? Resized; - - public event EventHandler? CloseRequested; - - public event EventHandler<(double X, double Y, int Button)>? PointerPressed; - - public event EventHandler<(double X, double Y, int Button)>? PointerReleased; - - public event EventHandler<(double X, double Y)>? PointerMoved; - - public GtkHostWindow(string title, int width, int height) - { - _width = width; - _height = height; - Environment.SetEnvironmentVariable("GDK_BACKEND", "x11"); - Environment.SetEnvironmentVariable("WEBKIT_DISABLE_SANDBOX_THIS_IS_DANGEROUS", "1"); - Environment.SetEnvironmentVariable("LIBGL_ALWAYS_SOFTWARE", "1"); - int argc = 0; - IntPtr argv = IntPtr.Zero; - if (!GtkNative.gtk_init_check(ref argc, ref argv)) - { - throw new InvalidOperationException("Failed to initialize GTK. Is a display available?"); - } - _window = GtkNative.gtk_window_new(0); - if (_window == IntPtr.Zero) - { - throw new InvalidOperationException("Failed to create GTK window"); - } - GtkNative.gtk_window_set_title(_window, title); - GtkNative.gtk_window_set_default_size(_window, width, height); - _overlay = GtkNative.gtk_overlay_new(); - GtkNative.gtk_container_add(_window, _overlay); - _skiaSurface = new GtkSkiaSurfaceWidget(width, height); - GtkNative.gtk_container_add(_overlay, _skiaSurface.Widget); - _webViewLayer = GtkNative.gtk_fixed_new(); - GtkNative.gtk_overlay_add_overlay(_overlay, _webViewLayer); - GtkNative.gtk_widget_set_can_focus(_webViewLayer, canFocus: false); - GtkNative.gtk_overlay_set_overlay_pass_through(_overlay, _webViewLayer, passThrough: true); - _deleteEventHandler = OnDeleteEvent; - _configureEventHandler = OnConfigureEvent; - _buttonPressHandler = OnButtonPress; - _buttonReleaseHandler = OnButtonRelease; - _motionHandler = OnMotion; - ConnectSignal(_window, "delete-event", Marshal.GetFunctionPointerForDelegate(_deleteEventHandler)); - ConnectSignal(_window, "configure-event", Marshal.GetFunctionPointerForDelegate(_configureEventHandler)); - GtkNative.gtk_widget_add_events(_window, 772); - ConnectSignal(_window, "button-press-event", Marshal.GetFunctionPointerForDelegate(_buttonPressHandler)); - ConnectSignal(_window, "button-release-event", Marshal.GetFunctionPointerForDelegate(_buttonReleaseHandler)); - ConnectSignal(_window, "motion-notify-event", Marshal.GetFunctionPointerForDelegate(_motionHandler)); - Console.WriteLine($"[GtkHostWindow] Created GTK window on X11: {width}x{height}"); - } - - private void ConnectSignal(IntPtr widget, string signal, IntPtr handler) - { - GtkNative.g_signal_connect_data(widget, signal, handler, IntPtr.Zero, IntPtr.Zero, 0); - } - - private bool OnDeleteEvent(IntPtr widget, IntPtr eventData, IntPtr userData) - { - this.CloseRequested?.Invoke(this, EventArgs.Empty); - _isRunning = false; - GtkNative.gtk_main_quit(); - return true; - } - - private bool OnConfigureEvent(IntPtr widget, IntPtr eventData, IntPtr userData) - { - GtkNative.gtk_window_get_size(_window, out var width, out var height); - if (width != _width || height != _height) - { - _width = width; - _height = height; - _skiaSurface?.Resize(width, height); - this.Resized?.Invoke(this, (_width, _height)); - } - return false; - } - - private bool OnButtonPress(IntPtr widget, IntPtr eventData, IntPtr userData) - { - (double x, double y, int button) tuple = ParseButtonEvent(eventData); - double item = tuple.x; - double item2 = tuple.y; - int item3 = tuple.button; - string value = item3 switch - { - 3 => "Right", - 2 => "Middle", - 1 => "Left", - _ => $"Other({item3})", - }; - Console.WriteLine($"[GtkHostWindow] ButtonPress at ({item:F1}, {item2:F1}), button={item3} ({value})"); - this.PointerPressed?.Invoke(this, (item, item2, item3)); - _skiaSurface?.RaisePointerPressed(item, item2, item3); - return false; - } - - private bool OnButtonRelease(IntPtr widget, IntPtr eventData, IntPtr userData) - { - var (num, num2, num3) = ParseButtonEvent(eventData); - this.PointerReleased?.Invoke(this, (num, num2, num3)); - _skiaSurface?.RaisePointerReleased(num, num2, num3); - return false; - } - - private bool OnMotion(IntPtr widget, IntPtr eventData, IntPtr userData) - { - var (num, num2) = ParseMotionEvent(eventData); - this.PointerMoved?.Invoke(this, (num, num2)); - _skiaSurface?.RaisePointerMoved(num, num2); - return false; - } - - private static (double x, double y, int button) ParseButtonEvent(IntPtr eventData) - { - GdkEventButton gdkEventButton = Marshal.PtrToStructure(eventData); - return (x: gdkEventButton.x, y: gdkEventButton.y, button: (int)gdkEventButton.button); - } - - private static (double x, double y) ParseMotionEvent(IntPtr eventData) - { - GdkEventMotion gdkEventMotion = Marshal.PtrToStructure(eventData); - return (x: gdkEventMotion.x, y: gdkEventMotion.y); - } - - public void Show() - { - GtkNative.gtk_widget_show_all(_window); - _isRunning = true; - } - - public void Hide() - { - GtkNative.gtk_widget_hide(_window); - } - - public void SetTitle(string title) - { - GtkNative.gtk_window_set_title(_window, title); - } - - public void SetIcon(string iconPath) - { - if (string.IsNullOrEmpty(iconPath) || !File.Exists(iconPath)) - { - Console.WriteLine("[GtkHostWindow] Icon file not found: " + iconPath); - return; - } - try - { - IntPtr intPtr = GtkNative.gdk_pixbuf_new_from_file(iconPath, IntPtr.Zero); - if (intPtr != IntPtr.Zero) - { - GtkNative.gtk_window_set_icon(_window, intPtr); - GtkNative.g_object_unref(intPtr); - Console.WriteLine("[GtkHostWindow] Set window icon: " + iconPath); - } - } - catch (Exception ex) - { - Console.WriteLine("[GtkHostWindow] Failed to set icon: " + ex.Message); - } - } - - public void Resize(int width, int height) - { - GtkNative.gtk_window_resize(_window, width, height); - } - - public void AddWebView(IntPtr webViewWidget, int x, int y, int width, int height) - { - GtkNative.gtk_widget_set_size_request(webViewWidget, width, height); - GtkNative.gtk_fixed_put(_webViewLayer, webViewWidget, x, y); - GtkNative.gtk_widget_show(webViewWidget); - Console.WriteLine($"[GtkHostWindow] Added WebView at ({x}, {y}) size {width}x{height}"); - } - - public void MoveResizeWebView(IntPtr webViewWidget, int x, int y, int width, int height) - { - GtkNative.gtk_widget_set_size_request(webViewWidget, width, height); - GtkNative.gtk_fixed_move(_webViewLayer, webViewWidget, x, y); - } - - public void RemoveWebView(IntPtr webViewWidget) - { - GtkNative.gtk_container_remove(_webViewLayer, webViewWidget); - } - - public void RequestRedraw() - { - if (_skiaSurface != null) - { - GtkNative.gtk_widget_queue_draw(_skiaSurface.Widget); - } - } - - public void Run() - { - Show(); - GtkNative.gtk_main(); - } - - public void Stop() - { - _isRunning = false; - GtkNative.gtk_main_quit(); - } - - public void ProcessEvents() - { - while (GtkNative.gtk_events_pending()) - { - GtkNative.gtk_main_iteration_do(blocking: false); - } - } - - public void Dispose() - { - if (!_disposed) - { - _disposed = true; - _skiaSurface?.Dispose(); - if (_window != IntPtr.Zero) - { - GtkNative.gtk_widget_destroy(_window); - _window = IntPtr.Zero; - } - } - } -} diff --git a/Window/WaylandWindow.cs b/Window/WaylandWindow.cs index e7b2387..7cdf811 100644 --- a/Window/WaylandWindow.cs +++ b/Window/WaylandWindow.cs @@ -1,1354 +1,1334 @@ -using System; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using System.Runtime.InteropServices; using Microsoft.Maui.Platform.Linux.Input; namespace Microsoft.Maui.Platform.Linux.Window; +/// +/// Native Wayland window implementation using xdg-shell protocol. +/// Provides full Wayland support without XWayland dependency. +/// public class WaylandWindow : IDisposable { - private struct WlInterface - { - public IntPtr Name; + #region Native Interop - libwayland-client - public int Version; + private const string LibWaylandClient = "libwayland-client.so.0"; - public int MethodCount; + // Core display functions (actually exported) + [DllImport(LibWaylandClient)] + private static extern IntPtr wl_display_connect(string? name); - public IntPtr Methods; + [DllImport(LibWaylandClient)] + private static extern void wl_display_disconnect(IntPtr display); - public int EventCount; + [DllImport(LibWaylandClient)] + private static extern int wl_display_dispatch(IntPtr display); - public IntPtr Events; - } + [DllImport(LibWaylandClient)] + private static extern int wl_display_dispatch_pending(IntPtr display); - private struct WlRegistryListener - { - public IntPtr Global; + [DllImport(LibWaylandClient)] + private static extern int wl_display_roundtrip(IntPtr display); - public IntPtr GlobalRemove; - } + [DllImport(LibWaylandClient)] + private static extern int wl_display_flush(IntPtr display); - private struct WlSurfaceListener - { - public IntPtr Enter; + [DllImport(LibWaylandClient)] + private static extern int wl_display_get_fd(IntPtr display); - public IntPtr Leave; - } + // Low-level proxy API (actually exported - used to implement protocol wrappers) + [DllImport(LibWaylandClient)] + private static extern IntPtr wl_proxy_marshal_constructor( + IntPtr proxy, uint opcode, IntPtr iface, IntPtr arg); - private struct WlBufferListener - { - public IntPtr Release; - } + [DllImport(LibWaylandClient)] + private static extern IntPtr wl_proxy_marshal_constructor_versioned( + IntPtr proxy, uint opcode, IntPtr iface, uint version, IntPtr arg); - private struct WlSeatListener - { - public IntPtr Capabilities; + [DllImport(LibWaylandClient)] + private static extern void wl_proxy_marshal(IntPtr proxy, uint opcode); - public IntPtr Name; - } + [DllImport(LibWaylandClient)] + private static extern void wl_proxy_marshal(IntPtr proxy, uint opcode, IntPtr arg1); - private struct WlPointerListener - { - public IntPtr Enter; + [DllImport(LibWaylandClient)] + private static extern void wl_proxy_marshal(IntPtr proxy, uint opcode, int arg1, int arg2); - public IntPtr Leave; + [DllImport(LibWaylandClient)] + private static extern void wl_proxy_marshal(IntPtr proxy, uint opcode, IntPtr arg1, int arg2, int arg3); - public IntPtr Motion; + [DllImport(LibWaylandClient)] + private static extern void wl_proxy_marshal(IntPtr proxy, uint opcode, int arg1, int arg2, int arg3, int arg4); - public IntPtr Button; + [DllImport(LibWaylandClient)] + private static extern void wl_proxy_marshal(IntPtr proxy, uint opcode, uint arg1); - public IntPtr Axis; + [DllImport(LibWaylandClient)] + private static extern void wl_proxy_marshal(IntPtr proxy, uint opcode, + [MarshalAs(UnmanagedType.LPStr)] string arg1); - public IntPtr Frame; + [DllImport(LibWaylandClient)] + private static extern IntPtr wl_proxy_marshal_array_constructor( + IntPtr proxy, uint opcode, IntPtr args, IntPtr iface); - public IntPtr AxisSource; + [DllImport(LibWaylandClient)] + private static extern IntPtr wl_proxy_marshal_array_constructor_versioned( + IntPtr proxy, uint opcode, IntPtr args, IntPtr iface, uint version); - public IntPtr AxisStop; + [DllImport(LibWaylandClient)] + private static extern int wl_proxy_add_listener(IntPtr proxy, IntPtr impl, IntPtr data); - public IntPtr AxisDiscrete; - } + [DllImport(LibWaylandClient)] + private static extern void wl_proxy_destroy(IntPtr proxy); - private struct WlKeyboardListener - { - public IntPtr Keymap; + [DllImport(LibWaylandClient)] + private static extern uint wl_proxy_get_version(IntPtr proxy); - public IntPtr Enter; + // Interface globals (exported as data symbols) + [DllImport(LibWaylandClient)] + private static extern IntPtr wl_registry_interface_ptr(); - public IntPtr Leave; + // We need to load these at runtime since they're data symbols + private static IntPtr _wl_registry_interface; + private static IntPtr _wl_compositor_interface; + private static IntPtr _wl_shm_interface; + private static IntPtr _wl_shm_pool_interface; + private static IntPtr _wl_buffer_interface; + private static IntPtr _wl_surface_interface; + private static IntPtr _wl_seat_interface; + private static IntPtr _wl_pointer_interface; + private static IntPtr _wl_keyboard_interface; - public IntPtr Key; + // dlsym for loading interface symbols + [DllImport("libdl.so.2", EntryPoint = "dlopen")] + private static extern IntPtr dlopen(string? filename, int flags); - public IntPtr Modifiers; + [DllImport("libdl.so.2", EntryPoint = "dlsym")] + private static extern IntPtr dlsym(IntPtr handle, string symbol); - public IntPtr RepeatInfo; - } + [DllImport("libdl.so.2", EntryPoint = "dlclose")] + private static extern int dlclose(IntPtr handle); - private struct XdgWmBaseListener - { - public IntPtr Ping; - } + private const int RTLD_NOW = 2; + private const int RTLD_GLOBAL = 0x100; - private struct XdgSurfaceListener - { - public IntPtr Configure; - } + #endregion - private struct XdgToplevelListener - { - public IntPtr Configure; + #region Wayland Protocol Opcodes - public IntPtr Close; - } + // wl_display opcodes + private const uint WL_DISPLAY_GET_REGISTRY = 1; - private delegate void RegistryGlobalDelegate(IntPtr data, IntPtr registry, uint name, IntPtr iface, uint version); - - private delegate void RegistryGlobalRemoveDelegate(IntPtr data, IntPtr registry, uint name); - - private delegate void SeatCapabilitiesDelegate(IntPtr data, IntPtr seat, uint capabilities); - - private delegate void SeatNameDelegate(IntPtr data, IntPtr seat, IntPtr name); - - private delegate void PointerEnterDelegate(IntPtr data, IntPtr pointer, uint serial, IntPtr surface, int x, int y); - - private delegate void PointerLeaveDelegate(IntPtr data, IntPtr pointer, uint serial, IntPtr surface); - - private delegate void PointerMotionDelegate(IntPtr data, IntPtr pointer, uint time, int x, int y); - - private delegate void PointerButtonDelegate(IntPtr data, IntPtr pointer, uint serial, uint time, uint button, uint state); - - private delegate void PointerAxisDelegate(IntPtr data, IntPtr pointer, uint time, uint axis, int value); - - private delegate void PointerFrameDelegate(IntPtr data, IntPtr pointer); - - private delegate void KeyboardKeymapDelegate(IntPtr data, IntPtr keyboard, uint format, int fd, uint size); - - private delegate void KeyboardEnterDelegate(IntPtr data, IntPtr keyboard, uint serial, IntPtr surface, IntPtr keys); - - private delegate void KeyboardLeaveDelegate(IntPtr data, IntPtr keyboard, uint serial, IntPtr surface); - - private delegate void KeyboardKeyDelegate(IntPtr data, IntPtr keyboard, uint serial, uint time, uint key, uint state); - - private delegate void KeyboardModifiersDelegate(IntPtr data, IntPtr keyboard, uint serial, uint modsDepressed, uint modsLatched, uint modsLocked, uint group); - - private delegate void XdgWmBasePingDelegate(IntPtr data, IntPtr wmBase, uint serial); - - private delegate void XdgSurfaceConfigureDelegate(IntPtr data, IntPtr xdgSurface, uint serial); - - private delegate void XdgToplevelConfigureDelegate(IntPtr data, IntPtr toplevel, int width, int height, IntPtr states); - - private delegate void XdgToplevelCloseDelegate(IntPtr data, IntPtr toplevel); - - private delegate void BufferReleaseDelegate(IntPtr data, IntPtr buffer); - - private const string LibWaylandClient = "libwayland-client.so.0"; - - private static IntPtr _wl_registry_interface; - - private static IntPtr _wl_compositor_interface; - - private static IntPtr _wl_shm_interface; - - private static IntPtr _wl_shm_pool_interface; - - private static IntPtr _wl_buffer_interface; - - private static IntPtr _wl_surface_interface; - - private static IntPtr _wl_seat_interface; - - private static IntPtr _wl_pointer_interface; - - private static IntPtr _wl_keyboard_interface; - - private const int RTLD_NOW = 2; - - private const int RTLD_GLOBAL = 256; - - private const uint WL_DISPLAY_GET_REGISTRY = 1u; - - private const uint WL_REGISTRY_BIND = 0u; - - private const uint WL_COMPOSITOR_CREATE_SURFACE = 0u; - - private const uint WL_SURFACE_DESTROY = 0u; - - private const uint WL_SURFACE_ATTACH = 1u; - - private const uint WL_SURFACE_DAMAGE = 2u; - - private const uint WL_SURFACE_COMMIT = 6u; - - private const uint WL_SURFACE_DAMAGE_BUFFER = 9u; - - private const uint WL_SHM_CREATE_POOL = 0u; - - private const uint WL_SHM_POOL_CREATE_BUFFER = 0u; - - private const uint WL_SHM_POOL_DESTROY = 1u; - - private const uint WL_BUFFER_DESTROY = 0u; - - private const uint WL_SEAT_GET_POINTER = 0u; - - private const uint WL_SEAT_GET_KEYBOARD = 1u; - - private const uint XDG_WM_BASE_GET_XDG_SURFACE = 2u; - - private const uint XDG_WM_BASE_PONG = 3u; - - private const uint XDG_SURFACE_DESTROY = 0u; - - private const uint XDG_SURFACE_GET_TOPLEVEL = 1u; - - private const uint XDG_SURFACE_ACK_CONFIGURE = 4u; - - private const uint XDG_TOPLEVEL_DESTROY = 0u; - - private const uint XDG_TOPLEVEL_SET_TITLE = 2u; - - private const uint XDG_TOPLEVEL_SET_APP_ID = 3u; - - private static IntPtr _xdg_wm_base_interface; - - private static IntPtr _xdg_surface_interface; - - private static IntPtr _xdg_toplevel_interface; - - private static GCHandle _xdgWmBaseInterfaceHandle; - - private static GCHandle _xdgSurfaceInterfaceHandle; - - private static GCHandle _xdgToplevelInterfaceHandle; - - private static IntPtr _xdgWmBaseName; - - private static IntPtr _xdgSurfaceName; - - private static IntPtr _xdgToplevelName; - - private const int O_RDWR = 2; - - private const int O_CREAT = 64; - - private const int O_EXCL = 128; - - private const int PROT_READ = 1; - - private const int PROT_WRITE = 2; - - private const int MAP_SHARED = 1; - - private const uint MFD_CLOEXEC = 1u; - - private const uint WL_SHM_FORMAT_ARGB8888 = 0u; - - private const uint WL_SHM_FORMAT_XRGB8888 = 1u; - - private const uint WL_SEAT_CAPABILITY_POINTER = 1u; - - private const uint WL_SEAT_CAPABILITY_KEYBOARD = 2u; - - private const uint WL_POINTER_BUTTON_STATE_RELEASED = 0u; - - private const uint WL_POINTER_BUTTON_STATE_PRESSED = 1u; - - private const uint BTN_LEFT = 272u; - - private const uint BTN_RIGHT = 273u; - - private const uint BTN_MIDDLE = 274u; - - private const uint WL_KEYBOARD_KEY_STATE_RELEASED = 0u; - - private const uint WL_KEYBOARD_KEY_STATE_PRESSED = 1u; - - private IntPtr _display; - - private IntPtr _registry; - - private IntPtr _compositor; - - private IntPtr _shm; - - private IntPtr _seat; - - private IntPtr _xdgWmBase; - - private IntPtr _surface; - - private IntPtr _xdgSurface; - - private IntPtr _xdgToplevel; - - private IntPtr _pointer; - - private IntPtr _keyboard; - - private IntPtr _shmPool; - - private IntPtr _buffer; - - private IntPtr _pixelData; - - private int _shmFd = -1; - - private int _bufferSize; - - private int _stride; - - private int _width; - - private int _height; - - private int _pendingWidth; - - private int _pendingHeight; - - private string _title; - - private bool _isRunning; - - private bool _disposed; - - private bool _configured; - - private uint _lastConfigureSerial; - - private float _pointerX; - - private float _pointerY; - - private uint _pointerSerial; - - private uint _modifiers; - - private WlRegistryListener _registryListener; - - private WlSeatListener _seatListener; - - private WlPointerListener _pointerListener; - - private WlKeyboardListener _keyboardListener; - - private XdgWmBaseListener _wmBaseListener; - - private XdgSurfaceListener _xdgSurfaceListener; - - private XdgToplevelListener _toplevelListener; - - private WlBufferListener _bufferListener; - - private GCHandle _registryListenerHandle; - - private GCHandle _seatListenerHandle; - - private GCHandle _pointerListenerHandle; - - private GCHandle _keyboardListenerHandle; - - private GCHandle _wmBaseListenerHandle; - - private GCHandle _xdgSurfaceListenerHandle; - - private GCHandle _toplevelListenerHandle; - - private GCHandle _bufferListenerHandle; - - private static bool _interfacesInitialized; - - private GCHandle _thisHandle; - - public IntPtr Display => _display; - - public IntPtr Surface => _surface; - - public int Width => _width; - - public int Height => _height; - - public bool IsRunning => _isRunning; - - public IntPtr PixelData => _pixelData; - - public int Stride => _stride; - - public event EventHandler? KeyDown; - - public event EventHandler? KeyUp; - - public event EventHandler? TextInput; - - public event EventHandler? PointerMoved; - - public event EventHandler? PointerPressed; - - public event EventHandler? PointerReleased; - - public event EventHandler? Scroll; - - public event EventHandler? Exposed; - - public event EventHandler<(int Width, int Height)>? Resized; - - public event EventHandler? CloseRequested; - - public event EventHandler? FocusGained; - - public event EventHandler? FocusLost; - - [DllImport("libwayland-client.so.0")] - private static extern IntPtr wl_display_connect(string? name); - - [DllImport("libwayland-client.so.0")] - private static extern void wl_display_disconnect(IntPtr display); - - [DllImport("libwayland-client.so.0")] - private static extern int wl_display_dispatch(IntPtr display); - - [DllImport("libwayland-client.so.0")] - private static extern int wl_display_dispatch_pending(IntPtr display); - - [DllImport("libwayland-client.so.0")] - private static extern int wl_display_roundtrip(IntPtr display); - - [DllImport("libwayland-client.so.0")] - private static extern int wl_display_flush(IntPtr display); - - [DllImport("libwayland-client.so.0")] - private static extern int wl_display_get_fd(IntPtr display); - - [DllImport("libwayland-client.so.0")] - private static extern IntPtr wl_proxy_marshal_constructor(IntPtr proxy, uint opcode, IntPtr iface, IntPtr arg); - - [DllImport("libwayland-client.so.0")] - private static extern IntPtr wl_proxy_marshal_constructor_versioned(IntPtr proxy, uint opcode, IntPtr iface, uint version, IntPtr arg); - - [DllImport("libwayland-client.so.0")] - private static extern void wl_proxy_marshal(IntPtr proxy, uint opcode); - - [DllImport("libwayland-client.so.0")] - private static extern void wl_proxy_marshal(IntPtr proxy, uint opcode, IntPtr arg1); - - [DllImport("libwayland-client.so.0")] - private static extern void wl_proxy_marshal(IntPtr proxy, uint opcode, int arg1, int arg2); - - [DllImport("libwayland-client.so.0")] - private static extern void wl_proxy_marshal(IntPtr proxy, uint opcode, IntPtr arg1, int arg2, int arg3); - - [DllImport("libwayland-client.so.0")] - private static extern void wl_proxy_marshal(IntPtr proxy, uint opcode, int arg1, int arg2, int arg3, int arg4); - - [DllImport("libwayland-client.so.0")] - private static extern void wl_proxy_marshal(IntPtr proxy, uint opcode, uint arg1); - - [DllImport("libwayland-client.so.0")] - private static extern void wl_proxy_marshal(IntPtr proxy, uint opcode, [MarshalAs(UnmanagedType.LPStr)] string arg1); - - [DllImport("libwayland-client.so.0")] - private static extern IntPtr wl_proxy_marshal_array_constructor(IntPtr proxy, uint opcode, IntPtr args, IntPtr iface); - - [DllImport("libwayland-client.so.0")] - private static extern IntPtr wl_proxy_marshal_array_constructor_versioned(IntPtr proxy, uint opcode, IntPtr args, IntPtr iface, uint version); - - [DllImport("libwayland-client.so.0")] - private static extern int wl_proxy_add_listener(IntPtr proxy, IntPtr impl, IntPtr data); - - [DllImport("libwayland-client.so.0")] - private static extern void wl_proxy_destroy(IntPtr proxy); - - [DllImport("libwayland-client.so.0")] - private static extern uint wl_proxy_get_version(IntPtr proxy); - - [DllImport("libwayland-client.so.0")] - private static extern IntPtr wl_registry_interface_ptr(); - - [DllImport("libdl.so.2")] - private static extern IntPtr dlopen(string? filename, int flags); - - [DllImport("libdl.so.2")] - private static extern IntPtr dlsym(IntPtr handle, string symbol); - - [DllImport("libdl.so.2")] - private static extern int dlclose(IntPtr handle); - - private static void LoadInterfaceSymbols() - { - if (_wl_registry_interface == IntPtr.Zero) - { - IntPtr intPtr = dlopen("libwayland-client.so.0", 258); - if (intPtr == IntPtr.Zero) - { - throw new InvalidOperationException("Failed to load libwayland-client.so.0"); - } - _wl_registry_interface = dlsym(intPtr, "wl_registry_interface"); - _wl_compositor_interface = dlsym(intPtr, "wl_compositor_interface"); - _wl_shm_interface = dlsym(intPtr, "wl_shm_interface"); - _wl_shm_pool_interface = dlsym(intPtr, "wl_shm_pool_interface"); - _wl_buffer_interface = dlsym(intPtr, "wl_buffer_interface"); - _wl_surface_interface = dlsym(intPtr, "wl_surface_interface"); - _wl_seat_interface = dlsym(intPtr, "wl_seat_interface"); - _wl_pointer_interface = dlsym(intPtr, "wl_pointer_interface"); - _wl_keyboard_interface = dlsym(intPtr, "wl_keyboard_interface"); - } - } - - private static IntPtr wl_display_get_registry(IntPtr display) - { - return wl_proxy_marshal_constructor(display, 1u, _wl_registry_interface, IntPtr.Zero); - } - - private static int wl_registry_add_listener(IntPtr registry, IntPtr listener, IntPtr data) - { - return wl_proxy_add_listener(registry, listener, data); - } - - [DllImport("libwayland-client.so.0")] - private static extern IntPtr wl_proxy_marshal_flags(IntPtr proxy, uint opcode, IntPtr iface, uint version, uint flags, uint name, IntPtr ifaceName, uint ifaceVersion); - - private static IntPtr wl_registry_bind(IntPtr registry, uint name, IntPtr iface, uint version) - { - return wl_proxy_marshal_flags(registry, 0u, iface, version, 0u, name, iface, version); - } - - private static IntPtr wl_compositor_create_surface(IntPtr compositor) - { - return wl_proxy_marshal_constructor(compositor, 0u, _wl_surface_interface, IntPtr.Zero); - } - - private static void wl_surface_attach(IntPtr surface, IntPtr buffer, int x, int y) - { - wl_proxy_marshal(surface, 1u, buffer, x, y); - } - - private static void wl_surface_damage(IntPtr surface, int x, int y, int width, int height) - { - wl_proxy_marshal(surface, 2u, x, y, width, height); - } - - private static void wl_surface_damage_buffer(IntPtr surface, int x, int y, int width, int height) - { - wl_proxy_marshal(surface, 9u, x, y, width, height); - } - - private static void wl_surface_commit(IntPtr surface) - { - wl_proxy_marshal(surface, 6u); - } - - private static void wl_surface_destroy(IntPtr surface) - { - wl_proxy_marshal(surface, 0u); - wl_proxy_destroy(surface); - } - - [DllImport("libwayland-client.so.0", EntryPoint = "wl_proxy_marshal_flags")] - private static extern IntPtr wl_proxy_marshal_flags_fd(IntPtr proxy, uint opcode, IntPtr iface, uint version, uint flags, IntPtr newId, int fd, int size); - - private static IntPtr wl_shm_create_pool(IntPtr shm, int fd, int size) - { - return wl_proxy_marshal_flags_fd(shm, 0u, _wl_shm_pool_interface, wl_proxy_get_version(shm), 0u, IntPtr.Zero, fd, size); - } - - [DllImport("libwayland-client.so.0", EntryPoint = "wl_proxy_marshal_flags")] - private static extern IntPtr wl_proxy_marshal_flags_buffer(IntPtr proxy, uint opcode, IntPtr iface, uint version, uint flags, IntPtr newId, int offset, int width, int height, int stride, uint format); - - private static IntPtr wl_shm_pool_create_buffer(IntPtr pool, int offset, int width, int height, int stride, uint format) - { - return wl_proxy_marshal_flags_buffer(pool, 0u, _wl_buffer_interface, wl_proxy_get_version(pool), 0u, IntPtr.Zero, offset, width, height, stride, format); - } - - private static void wl_shm_pool_destroy(IntPtr pool) - { - wl_proxy_marshal(pool, 1u); - wl_proxy_destroy(pool); - } - - private static void wl_buffer_destroy(IntPtr buffer) - { - wl_proxy_marshal(buffer, 0u); - wl_proxy_destroy(buffer); - } - - private static int wl_buffer_add_listener(IntPtr buffer, IntPtr listener, IntPtr data) - { - return wl_proxy_add_listener(buffer, listener, data); - } - - private static int wl_seat_add_listener(IntPtr seat, IntPtr listener, IntPtr data) - { - return wl_proxy_add_listener(seat, listener, data); - } - - private static IntPtr wl_seat_get_pointer(IntPtr seat) - { - return wl_proxy_marshal_constructor(seat, 0u, _wl_pointer_interface, IntPtr.Zero); - } - - private static IntPtr wl_seat_get_keyboard(IntPtr seat) - { - return wl_proxy_marshal_constructor(seat, 1u, _wl_keyboard_interface, IntPtr.Zero); - } - - private static int wl_pointer_add_listener(IntPtr pointer, IntPtr listener, IntPtr data) - { - return wl_proxy_add_listener(pointer, listener, data); - } - - private static int wl_keyboard_add_listener(IntPtr keyboard, IntPtr listener, IntPtr data) - { - return wl_proxy_add_listener(keyboard, listener, data); - } - - private static void LoadXdgShellInterfaces() - { - if (_xdg_wm_base_interface == IntPtr.Zero) - { - _xdgWmBaseName = Marshal.StringToHGlobalAnsi("xdg_wm_base"); - _xdgSurfaceName = Marshal.StringToHGlobalAnsi("xdg_surface"); - _xdgToplevelName = Marshal.StringToHGlobalAnsi("xdg_toplevel"); - _xdgWmBaseInterfaceHandle = GCHandle.Alloc(new WlInterface - { - Name = _xdgWmBaseName, - Version = 6, - MethodCount = 4, - Methods = IntPtr.Zero, - EventCount = 1, - Events = IntPtr.Zero - }, GCHandleType.Pinned); - _xdg_wm_base_interface = _xdgWmBaseInterfaceHandle.AddrOfPinnedObject(); - _xdgSurfaceInterfaceHandle = GCHandle.Alloc(new WlInterface - { - Name = _xdgSurfaceName, - Version = 6, - MethodCount = 5, - Methods = IntPtr.Zero, - EventCount = 1, - Events = IntPtr.Zero - }, GCHandleType.Pinned); - _xdg_surface_interface = _xdgSurfaceInterfaceHandle.AddrOfPinnedObject(); - _xdgToplevelInterfaceHandle = GCHandle.Alloc(new WlInterface - { - Name = _xdgToplevelName, - Version = 6, - MethodCount = 14, - Methods = IntPtr.Zero, - EventCount = 4, - Events = IntPtr.Zero - }, GCHandleType.Pinned); - _xdg_toplevel_interface = _xdgToplevelInterfaceHandle.AddrOfPinnedObject(); - } - } - - private static IntPtr xdg_wm_base_get_xdg_surface(IntPtr wmBase, IntPtr surface) - { - return wl_proxy_marshal_constructor(wmBase, 2u, _xdg_surface_interface, surface); - } - - private static void xdg_wm_base_pong(IntPtr wmBase, uint serial) - { - wl_proxy_marshal(wmBase, 3u, serial); - } - - private static int xdg_wm_base_add_listener(IntPtr wmBase, IntPtr listener, IntPtr data) - { - return wl_proxy_add_listener(wmBase, listener, data); - } - - private static IntPtr xdg_surface_get_toplevel(IntPtr xdgSurface) - { - return wl_proxy_marshal_constructor(xdgSurface, 1u, _xdg_toplevel_interface, IntPtr.Zero); - } - - private static void xdg_surface_ack_configure(IntPtr xdgSurface, uint serial) - { - wl_proxy_marshal(xdgSurface, 4u, serial); - } - - private static int xdg_surface_add_listener(IntPtr xdgSurface, IntPtr listener, IntPtr data) - { - return wl_proxy_add_listener(xdgSurface, listener, data); - } - - private static void xdg_surface_destroy(IntPtr xdgSurface) - { - wl_proxy_marshal(xdgSurface, 0u); - wl_proxy_destroy(xdgSurface); - } - - private static void xdg_toplevel_set_title(IntPtr toplevel, string title) - { - wl_proxy_marshal(toplevel, 2u, title); - } - - private static void xdg_toplevel_set_app_id(IntPtr toplevel, string appId) - { - wl_proxy_marshal(toplevel, 3u, appId); - } - - private static int xdg_toplevel_add_listener(IntPtr toplevel, IntPtr listener, IntPtr data) - { - return wl_proxy_add_listener(toplevel, listener, data); - } - - private static void xdg_toplevel_destroy(IntPtr toplevel) - { - wl_proxy_marshal(toplevel, 0u); - wl_proxy_destroy(toplevel); - } - - [DllImport("libc")] - private static extern int shm_open([MarshalAs(UnmanagedType.LPStr)] string name, int oflag, int mode); - - [DllImport("libc")] - private static extern int shm_unlink([MarshalAs(UnmanagedType.LPStr)] string name); - - [DllImport("libc")] - private static extern int ftruncate(int fd, long length); - - [DllImport("libc")] - private static extern IntPtr mmap(IntPtr addr, UIntPtr length, int prot, int flags, int fd, long offset); - - [DllImport("libc")] - private static extern int munmap(IntPtr addr, UIntPtr length); - - [DllImport("libc")] - private static extern int close(int fd); - - [DllImport("libc")] - private static extern int memfd_create([MarshalAs(UnmanagedType.LPStr)] string name, uint flags); - - public WaylandWindow(string title, int width, int height) - { - _title = title; - _width = width; - _height = height; - _pendingWidth = width; - _pendingHeight = height; - InitializeInterfaces(); - Initialize(); - } - - private static void InitializeInterfaces() - { - if (!_interfacesInitialized) - { - LoadInterfaceSymbols(); - LoadXdgShellInterfaces(); - _interfacesInitialized = true; - } - } - - private void Initialize() - { - _thisHandle = GCHandle.Alloc(this); - _display = wl_display_connect(null); - if (_display == IntPtr.Zero) - { - throw new InvalidOperationException("Failed to connect to Wayland display. Ensure WAYLAND_DISPLAY is set and a compositor is running."); - } - _registry = wl_display_get_registry(_display); - if (_registry == IntPtr.Zero) - { - throw new InvalidOperationException("Failed to get Wayland registry"); - } - _registryListener = new WlRegistryListener - { - Global = Marshal.GetFunctionPointerForDelegate(RegistryGlobal), - GlobalRemove = Marshal.GetFunctionPointerForDelegate(RegistryGlobalRemove) - }; - _registryListenerHandle = GCHandle.Alloc(_registryListener, GCHandleType.Pinned); - wl_registry_add_listener(_registry, _registryListenerHandle.AddrOfPinnedObject(), GCHandle.ToIntPtr(_thisHandle)); - wl_display_roundtrip(_display); - if (_compositor == IntPtr.Zero) - { - throw new InvalidOperationException("Wayland compositor not found"); - } - if (_shm == IntPtr.Zero) - { - throw new InvalidOperationException("Wayland shm not found"); - } - if (_xdgWmBase == IntPtr.Zero) - { - throw new InvalidOperationException("xdg_wm_base not found - compositor doesn't support xdg-shell"); - } - _surface = wl_compositor_create_surface(_compositor); - if (_surface == IntPtr.Zero) - { - throw new InvalidOperationException("Failed to create Wayland surface"); - } - _xdgSurface = xdg_wm_base_get_xdg_surface(_xdgWmBase, _surface); - if (_xdgSurface == IntPtr.Zero) - { - throw new InvalidOperationException("Failed to create xdg_surface"); - } - _xdgSurfaceListener = new XdgSurfaceListener - { - Configure = Marshal.GetFunctionPointerForDelegate(XdgSurfaceConfigure) - }; - _xdgSurfaceListenerHandle = GCHandle.Alloc(_xdgSurfaceListener, GCHandleType.Pinned); - xdg_surface_add_listener(_xdgSurface, _xdgSurfaceListenerHandle.AddrOfPinnedObject(), GCHandle.ToIntPtr(_thisHandle)); - _xdgToplevel = xdg_surface_get_toplevel(_xdgSurface); - if (_xdgToplevel == IntPtr.Zero) - { - throw new InvalidOperationException("Failed to create xdg_toplevel"); - } - _toplevelListener = new XdgToplevelListener - { - Configure = Marshal.GetFunctionPointerForDelegate(XdgToplevelConfigure), - Close = Marshal.GetFunctionPointerForDelegate(XdgToplevelClose) - }; - _toplevelListenerHandle = GCHandle.Alloc(_toplevelListener, GCHandleType.Pinned); - xdg_toplevel_add_listener(_xdgToplevel, _toplevelListenerHandle.AddrOfPinnedObject(), GCHandle.ToIntPtr(_thisHandle)); - xdg_toplevel_set_title(_xdgToplevel, _title); - xdg_toplevel_set_app_id(_xdgToplevel, "com.openmaui.app"); - wl_surface_commit(_surface); - wl_display_roundtrip(_display); - CreateShmBuffer(); - Console.WriteLine($"[Wayland] Window created: {_width}x{_height}"); - } - - private void CreateShmBuffer() - { - _stride = _width * 4; - _bufferSize = _stride * _height; - _shmFd = memfd_create("maui-buffer", 1u); - if (_shmFd < 0) - { - string name = $"/maui-{Environment.ProcessId}-{DateTime.Now.Ticks}"; - _shmFd = shm_open(name, 194, 384); - if (_shmFd >= 0) - { - shm_unlink(name); - } - } - if (_shmFd < 0) - { - throw new InvalidOperationException("Failed to create shared memory"); - } - if (ftruncate(_shmFd, _bufferSize) < 0) - { - close(_shmFd); - throw new InvalidOperationException("Failed to resize shared memory"); - } - _pixelData = mmap(IntPtr.Zero, (nuint)_bufferSize, 3, 1, _shmFd, 0L); - if (_pixelData == IntPtr.Zero || _pixelData == new IntPtr(-1)) - { - close(_shmFd); - throw new InvalidOperationException("Failed to mmap shared memory"); - } - _shmPool = wl_shm_create_pool(_shm, _shmFd, _bufferSize); - if (_shmPool == IntPtr.Zero) - { - munmap(_pixelData, (nuint)_bufferSize); - close(_shmFd); - throw new InvalidOperationException("Failed to create wl_shm_pool"); - } - _buffer = wl_shm_pool_create_buffer(_shmPool, 0, _width, _height, _stride, 0u); - if (_buffer == IntPtr.Zero) - { - wl_shm_pool_destroy(_shmPool); - munmap(_pixelData, (nuint)_bufferSize); - close(_shmFd); - throw new InvalidOperationException("Failed to create wl_buffer"); - } - _bufferListener = new WlBufferListener - { - Release = Marshal.GetFunctionPointerForDelegate(BufferRelease) - }; - if (_bufferListenerHandle.IsAllocated) - { - _bufferListenerHandle.Free(); - } - _bufferListenerHandle = GCHandle.Alloc(_bufferListener, GCHandleType.Pinned); - wl_buffer_add_listener(_buffer, _bufferListenerHandle.AddrOfPinnedObject(), GCHandle.ToIntPtr(_thisHandle)); - } - - private void ResizeBuffer(int newWidth, int newHeight) - { - if ((newWidth != _width || newHeight != _height) && newWidth > 0 && newHeight > 0) - { - if (_buffer != IntPtr.Zero) - { - wl_buffer_destroy(_buffer); - } - if (_shmPool != IntPtr.Zero) - { - wl_shm_pool_destroy(_shmPool); - } - if (_pixelData != IntPtr.Zero && _pixelData != new IntPtr(-1)) - { - munmap(_pixelData, (nuint)_bufferSize); - } - if (_shmFd >= 0) - { - close(_shmFd); - } - _width = newWidth; - _height = newHeight; - CreateShmBuffer(); - this.Resized?.Invoke(this, (_width, _height)); - } - } - - private static void RegistryGlobal(IntPtr data, IntPtr registry, uint name, IntPtr iface, uint version) - { - GCHandle gCHandle = GCHandle.FromIntPtr(data); - if (gCHandle.IsAllocated) - { - WaylandWindow waylandWindow = (WaylandWindow)gCHandle.Target; - string text = Marshal.PtrToStringAnsi(iface); - Console.WriteLine($"[Wayland] Global: {text} v{version}"); - switch (text) - { - case "wl_compositor": - waylandWindow._compositor = wl_registry_bind(registry, name, _wl_compositor_interface, Math.Min(version, 4u)); - break; - case "wl_shm": - waylandWindow._shm = wl_registry_bind(registry, name, _wl_shm_interface, 1u); - break; - case "wl_seat": - waylandWindow._seat = wl_registry_bind(registry, name, _wl_seat_interface, Math.Min(version, 5u)); - waylandWindow.SetupSeat(); - break; - case "xdg_wm_base": - waylandWindow._xdgWmBase = wl_registry_bind(registry, name, _xdg_wm_base_interface, Math.Min(version, 2u)); - waylandWindow.SetupXdgWmBase(); - break; - } - } - } - - private static void RegistryGlobalRemove(IntPtr data, IntPtr registry, uint name) - { - } - - private void SetupSeat() - { - if (_seat != IntPtr.Zero) - { - _seatListener = new WlSeatListener - { - Capabilities = Marshal.GetFunctionPointerForDelegate(SeatCapabilities), - Name = Marshal.GetFunctionPointerForDelegate(SeatName) - }; - _seatListenerHandle = GCHandle.Alloc(_seatListener, GCHandleType.Pinned); - wl_seat_add_listener(_seat, _seatListenerHandle.AddrOfPinnedObject(), GCHandle.ToIntPtr(_thisHandle)); - } - } - - private static void SeatCapabilities(IntPtr data, IntPtr seat, uint capabilities) - { - GCHandle gCHandle = GCHandle.FromIntPtr(data); - if (gCHandle.IsAllocated) - { - WaylandWindow waylandWindow = (WaylandWindow)gCHandle.Target; - if ((capabilities & 1) != 0 && waylandWindow._pointer == IntPtr.Zero) - { - waylandWindow._pointer = wl_seat_get_pointer(seat); - waylandWindow.SetupPointer(); - } - if ((capabilities & 2) != 0 && waylandWindow._keyboard == IntPtr.Zero) - { - waylandWindow._keyboard = wl_seat_get_keyboard(seat); - waylandWindow.SetupKeyboard(); - } - } - } - - private static void SeatName(IntPtr data, IntPtr seat, IntPtr name) - { - } - - private void SetupPointer() - { - if (_pointer != IntPtr.Zero) - { - _pointerListener = new WlPointerListener - { - Enter = Marshal.GetFunctionPointerForDelegate(PointerEnter), - Leave = Marshal.GetFunctionPointerForDelegate(PointerLeave), - Motion = Marshal.GetFunctionPointerForDelegate(PointerMotion), - Button = Marshal.GetFunctionPointerForDelegate(OnPointerButton), - Axis = Marshal.GetFunctionPointerForDelegate(PointerAxis), - Frame = Marshal.GetFunctionPointerForDelegate(PointerFrame) - }; - _pointerListenerHandle = GCHandle.Alloc(_pointerListener, GCHandleType.Pinned); - wl_pointer_add_listener(_pointer, _pointerListenerHandle.AddrOfPinnedObject(), GCHandle.ToIntPtr(_thisHandle)); - } - } - - private static void PointerEnter(IntPtr data, IntPtr pointer, uint serial, IntPtr surface, int x, int y) - { - GCHandle gCHandle = GCHandle.FromIntPtr(data); - if (gCHandle.IsAllocated) - { - WaylandWindow obj = (WaylandWindow)gCHandle.Target; - obj._pointerSerial = serial; - obj._pointerX = (float)x / 256f; - obj._pointerY = (float)y / 256f; - } - } - - private static void PointerLeave(IntPtr data, IntPtr pointer, uint serial, IntPtr surface) - { - } - - private static void PointerMotion(IntPtr data, IntPtr pointer, uint time, int x, int y) - { - GCHandle gCHandle = GCHandle.FromIntPtr(data); - if (gCHandle.IsAllocated) - { - WaylandWindow waylandWindow = (WaylandWindow)gCHandle.Target; - waylandWindow._pointerX = (float)x / 256f; - waylandWindow._pointerY = (float)y / 256f; - waylandWindow.PointerMoved?.Invoke(waylandWindow, new PointerEventArgs((int)waylandWindow._pointerX, (int)waylandWindow._pointerY)); - } - } - - private static void OnPointerButton(IntPtr data, IntPtr pointer, uint serial, uint time, uint button, uint state) - { - GCHandle gCHandle = GCHandle.FromIntPtr(data); - if (gCHandle.IsAllocated) - { - WaylandWindow waylandWindow = (WaylandWindow)gCHandle.Target; - PointerEventArgs e = new PointerEventArgs((int)waylandWindow._pointerX, (int)waylandWindow._pointerY, button switch - { - 272u => PointerButton.Left, - 273u => PointerButton.Right, - 274u => PointerButton.Middle, - _ => PointerButton.None, - }); - if (state == 1) - { - waylandWindow.PointerPressed?.Invoke(waylandWindow, e); - } - else - { - waylandWindow.PointerReleased?.Invoke(waylandWindow, e); - } - } - } - - private static void PointerAxis(IntPtr data, IntPtr pointer, uint time, uint axis, int value) - { - GCHandle gCHandle = GCHandle.FromIntPtr(data); - if (gCHandle.IsAllocated) - { - WaylandWindow waylandWindow = (WaylandWindow)gCHandle.Target; - float num = (float)value / 256f / 10f; - if (axis == 0) - { - waylandWindow.Scroll?.Invoke(waylandWindow, new ScrollEventArgs((int)waylandWindow._pointerX, (int)waylandWindow._pointerY, 0f, num)); - } - else - { - waylandWindow.Scroll?.Invoke(waylandWindow, new ScrollEventArgs((int)waylandWindow._pointerX, (int)waylandWindow._pointerY, num, 0f)); - } - } - } - - private static void PointerFrame(IntPtr data, IntPtr pointer) - { - } - - private void SetupKeyboard() - { - if (_keyboard != IntPtr.Zero) - { - _keyboardListener = new WlKeyboardListener - { - Keymap = Marshal.GetFunctionPointerForDelegate(KeyboardKeymap), - Enter = Marshal.GetFunctionPointerForDelegate(KeyboardEnter), - Leave = Marshal.GetFunctionPointerForDelegate(KeyboardLeave), - Key = Marshal.GetFunctionPointerForDelegate(KeyboardKey), - Modifiers = Marshal.GetFunctionPointerForDelegate(KeyboardModifiers) - }; - _keyboardListenerHandle = GCHandle.Alloc(_keyboardListener, GCHandleType.Pinned); - wl_keyboard_add_listener(_keyboard, _keyboardListenerHandle.AddrOfPinnedObject(), GCHandle.ToIntPtr(_thisHandle)); - } - } - - private static void KeyboardKeymap(IntPtr data, IntPtr keyboard, uint format, int fd, uint size) - { - close(fd); - } - - private static void KeyboardEnter(IntPtr data, IntPtr keyboard, uint serial, IntPtr surface, IntPtr keys) - { - GCHandle gCHandle = GCHandle.FromIntPtr(data); - if (gCHandle.IsAllocated) - { - WaylandWindow waylandWindow = (WaylandWindow)gCHandle.Target; - waylandWindow.FocusGained?.Invoke(waylandWindow, EventArgs.Empty); - } - } - - private static void KeyboardLeave(IntPtr data, IntPtr keyboard, uint serial, IntPtr surface) - { - GCHandle gCHandle = GCHandle.FromIntPtr(data); - if (gCHandle.IsAllocated) - { - WaylandWindow waylandWindow = (WaylandWindow)gCHandle.Target; - waylandWindow.FocusLost?.Invoke(waylandWindow, EventArgs.Empty); - } - } - - private static void KeyboardKey(IntPtr data, IntPtr keyboard, uint serial, uint time, uint keycode, uint state) - { - GCHandle gCHandle = GCHandle.FromIntPtr(data); - if (!gCHandle.IsAllocated) - { - return; - } - WaylandWindow waylandWindow = (WaylandWindow)gCHandle.Target; - Key key = KeyMapping.FromLinuxKeycode(keycode + 8); - KeyModifiers modifiers = (KeyModifiers)waylandWindow._modifiers; - KeyEventArgs e = new KeyEventArgs(key, modifiers); - if (state == 1) - { - waylandWindow.KeyDown?.Invoke(waylandWindow, e); - char? c = KeyMapping.ToChar(key, modifiers); - if (c.HasValue) - { - waylandWindow.TextInput?.Invoke(waylandWindow, new TextInputEventArgs(c.Value.ToString())); - } - } - else - { - waylandWindow.KeyUp?.Invoke(waylandWindow, e); - } - } - - private static void KeyboardModifiers(IntPtr data, IntPtr keyboard, uint serial, uint modsDepressed, uint modsLatched, uint modsLocked, uint group) - { - GCHandle gCHandle = GCHandle.FromIntPtr(data); - if (gCHandle.IsAllocated) - { - ((WaylandWindow)gCHandle.Target)._modifiers = modsDepressed | modsLatched; - } - } - - private void SetupXdgWmBase() - { - if (_xdgWmBase != IntPtr.Zero) - { - _wmBaseListener = new XdgWmBaseListener - { - Ping = Marshal.GetFunctionPointerForDelegate(XdgWmBasePing) - }; - _wmBaseListenerHandle = GCHandle.Alloc(_wmBaseListener, GCHandleType.Pinned); - xdg_wm_base_add_listener(_xdgWmBase, _wmBaseListenerHandle.AddrOfPinnedObject(), GCHandle.ToIntPtr(_thisHandle)); - } - } - - private static void XdgWmBasePing(IntPtr data, IntPtr wmBase, uint serial) - { - xdg_wm_base_pong(wmBase, serial); - } - - private static void XdgSurfaceConfigure(IntPtr data, IntPtr xdgSurface, uint serial) - { - GCHandle gCHandle = GCHandle.FromIntPtr(data); - if (!gCHandle.IsAllocated) - { - return; - } - WaylandWindow waylandWindow = (WaylandWindow)gCHandle.Target; - xdg_surface_ack_configure(xdgSurface, serial); - waylandWindow._lastConfigureSerial = serial; - if (!waylandWindow._configured) - { - waylandWindow._configured = true; - if (waylandWindow._pendingWidth > 0 && waylandWindow._pendingHeight > 0) - { - waylandWindow.ResizeBuffer(waylandWindow._pendingWidth, waylandWindow._pendingHeight); - } - waylandWindow.Exposed?.Invoke(waylandWindow, EventArgs.Empty); - } - } - - private static void XdgToplevelConfigure(IntPtr data, IntPtr toplevel, int width, int height, IntPtr states) - { - GCHandle gCHandle = GCHandle.FromIntPtr(data); - if (!gCHandle.IsAllocated) - { - return; - } - WaylandWindow waylandWindow = (WaylandWindow)gCHandle.Target; - if (width > 0 && height > 0) - { - waylandWindow._pendingWidth = width; - waylandWindow._pendingHeight = height; - if (waylandWindow._configured) - { - waylandWindow.ResizeBuffer(width, height); - } - } - } - - private static void XdgToplevelClose(IntPtr data, IntPtr toplevel) - { - GCHandle gCHandle = GCHandle.FromIntPtr(data); - if (gCHandle.IsAllocated) - { - WaylandWindow waylandWindow = (WaylandWindow)gCHandle.Target; - waylandWindow.CloseRequested?.Invoke(waylandWindow, EventArgs.Empty); - waylandWindow._isRunning = false; - } - } - - private static void BufferRelease(IntPtr data, IntPtr buffer) - { - } - - public void Show() - { - _isRunning = true; - wl_surface_attach(_surface, _buffer, 0, 0); - wl_surface_damage_buffer(_surface, 0, 0, _width, _height); - wl_surface_commit(_surface); - wl_display_flush(_display); - } - - public void Hide() - { - wl_surface_attach(_surface, IntPtr.Zero, 0, 0); - wl_surface_commit(_surface); - wl_display_flush(_display); - } - - public void SetTitle(string title) - { - _title = title; - if (_xdgToplevel != IntPtr.Zero) - { - xdg_toplevel_set_title(_xdgToplevel, title); - } - } - - public void Resize(int width, int height) - { - ResizeBuffer(width, height); - } - - public void ProcessEvents() - { - if (_isRunning && _display != IntPtr.Zero) - { - wl_display_dispatch_pending(_display); - wl_display_flush(_display); - } - } - - public void Stop() - { - _isRunning = false; - } - - public void CommitFrame() - { - if (_surface != IntPtr.Zero && _buffer != IntPtr.Zero) - { - wl_surface_attach(_surface, _buffer, 0, 0); - wl_surface_damage_buffer(_surface, 0, 0, _width, _height); - wl_surface_commit(_surface); - wl_display_flush(_display); - } - } - - public int GetFileDescriptor() - { - return wl_display_get_fd(_display); - } - - public void Dispose() - { - if (!_disposed) - { - _disposed = true; - _isRunning = false; - if (_buffer != IntPtr.Zero) - { - wl_buffer_destroy(_buffer); - _buffer = IntPtr.Zero; - } - if (_shmPool != IntPtr.Zero) - { - wl_shm_pool_destroy(_shmPool); - _shmPool = IntPtr.Zero; - } - if (_pixelData != IntPtr.Zero && _pixelData != new IntPtr(-1)) - { - munmap(_pixelData, (nuint)_bufferSize); - _pixelData = IntPtr.Zero; - } - if (_shmFd >= 0) - { - close(_shmFd); - _shmFd = -1; - } - if (_xdgToplevel != IntPtr.Zero) - { - xdg_toplevel_destroy(_xdgToplevel); - _xdgToplevel = IntPtr.Zero; - } - if (_xdgSurface != IntPtr.Zero) - { - xdg_surface_destroy(_xdgSurface); - _xdgSurface = IntPtr.Zero; - } - if (_surface != IntPtr.Zero) - { - wl_surface_destroy(_surface); - _surface = IntPtr.Zero; - } - if (_display != IntPtr.Zero) - { - wl_display_disconnect(_display); - _display = IntPtr.Zero; - } - if (_registryListenerHandle.IsAllocated) - { - _registryListenerHandle.Free(); - } - if (_seatListenerHandle.IsAllocated) - { - _seatListenerHandle.Free(); - } - if (_pointerListenerHandle.IsAllocated) - { - _pointerListenerHandle.Free(); - } - if (_keyboardListenerHandle.IsAllocated) - { - _keyboardListenerHandle.Free(); - } - if (_wmBaseListenerHandle.IsAllocated) - { - _wmBaseListenerHandle.Free(); - } - if (_xdgSurfaceListenerHandle.IsAllocated) - { - _xdgSurfaceListenerHandle.Free(); - } - if (_toplevelListenerHandle.IsAllocated) - { - _toplevelListenerHandle.Free(); - } - if (_bufferListenerHandle.IsAllocated) - { - _bufferListenerHandle.Free(); - } - if (_thisHandle.IsAllocated) - { - _thisHandle.Free(); - } - GC.SuppressFinalize(this); - } - } - - ~WaylandWindow() - { - Dispose(); - } + // wl_registry opcodes + private const uint WL_REGISTRY_BIND = 0; + + // wl_compositor opcodes + private const uint WL_COMPOSITOR_CREATE_SURFACE = 0; + + // wl_surface opcodes + private const uint WL_SURFACE_DESTROY = 0; + private const uint WL_SURFACE_ATTACH = 1; + private const uint WL_SURFACE_DAMAGE = 2; + private const uint WL_SURFACE_COMMIT = 6; + private const uint WL_SURFACE_DAMAGE_BUFFER = 9; + + // wl_shm opcodes + private const uint WL_SHM_CREATE_POOL = 0; + + // wl_shm_pool opcodes + private const uint WL_SHM_POOL_CREATE_BUFFER = 0; + private const uint WL_SHM_POOL_DESTROY = 1; + + // wl_buffer opcodes + private const uint WL_BUFFER_DESTROY = 0; + + // wl_seat opcodes + private const uint WL_SEAT_GET_POINTER = 0; + private const uint WL_SEAT_GET_KEYBOARD = 1; + + // xdg_wm_base opcodes + private const uint XDG_WM_BASE_GET_XDG_SURFACE = 2; + private const uint XDG_WM_BASE_PONG = 3; + + // xdg_surface opcodes + private const uint XDG_SURFACE_DESTROY = 0; + private const uint XDG_SURFACE_GET_TOPLEVEL = 1; + private const uint XDG_SURFACE_ACK_CONFIGURE = 4; + + // xdg_toplevel opcodes + private const uint XDG_TOPLEVEL_DESTROY = 0; + private const uint XDG_TOPLEVEL_SET_TITLE = 2; + private const uint XDG_TOPLEVEL_SET_APP_ID = 3; + + #endregion + + #region Protocol Wrapper Methods + + private static void LoadInterfaceSymbols() + { + if (_wl_registry_interface != IntPtr.Zero) return; + + var handle = dlopen("libwayland-client.so.0", RTLD_NOW | RTLD_GLOBAL); + if (handle == IntPtr.Zero) + throw new InvalidOperationException("Failed to load libwayland-client.so.0"); + + _wl_registry_interface = dlsym(handle, "wl_registry_interface"); + _wl_compositor_interface = dlsym(handle, "wl_compositor_interface"); + _wl_shm_interface = dlsym(handle, "wl_shm_interface"); + _wl_shm_pool_interface = dlsym(handle, "wl_shm_pool_interface"); + _wl_buffer_interface = dlsym(handle, "wl_buffer_interface"); + _wl_surface_interface = dlsym(handle, "wl_surface_interface"); + _wl_seat_interface = dlsym(handle, "wl_seat_interface"); + _wl_pointer_interface = dlsym(handle, "wl_pointer_interface"); + _wl_keyboard_interface = dlsym(handle, "wl_keyboard_interface"); + + // Don't close - we need the symbols to remain valid + } + + // wl_display_get_registry wrapper + private static IntPtr wl_display_get_registry(IntPtr display) + { + return wl_proxy_marshal_constructor(display, WL_DISPLAY_GET_REGISTRY, + _wl_registry_interface, IntPtr.Zero); + } + + // wl_registry_add_listener wrapper + private static int wl_registry_add_listener(IntPtr registry, IntPtr listener, IntPtr data) + { + return wl_proxy_add_listener(registry, listener, data); + } + + // wl_registry_bind wrapper - uses special marshaling + [DllImport(LibWaylandClient, EntryPoint = "wl_proxy_marshal_flags")] + private static extern IntPtr wl_proxy_marshal_flags( + IntPtr proxy, uint opcode, IntPtr iface, uint version, uint flags, + uint name, IntPtr ifaceName, uint ifaceVersion); + + private static IntPtr wl_registry_bind(IntPtr registry, uint name, IntPtr iface, uint version) + { + // For registry bind, we need to use marshal_flags with the interface + return wl_proxy_marshal_flags(registry, WL_REGISTRY_BIND, iface, version, 0, + name, iface, version); + } + + // wl_compositor_create_surface wrapper + private static IntPtr wl_compositor_create_surface(IntPtr compositor) + { + return wl_proxy_marshal_constructor(compositor, WL_COMPOSITOR_CREATE_SURFACE, + _wl_surface_interface, IntPtr.Zero); + } + + // wl_surface methods + private static void wl_surface_attach(IntPtr surface, IntPtr buffer, int x, int y) + { + wl_proxy_marshal(surface, WL_SURFACE_ATTACH, buffer, x, y); + } + + private static void wl_surface_damage(IntPtr surface, int x, int y, int width, int height) + { + wl_proxy_marshal(surface, WL_SURFACE_DAMAGE, x, y, width, height); + } + + private static void wl_surface_damage_buffer(IntPtr surface, int x, int y, int width, int height) + { + wl_proxy_marshal(surface, WL_SURFACE_DAMAGE_BUFFER, x, y, width, height); + } + + private static void wl_surface_commit(IntPtr surface) + { + wl_proxy_marshal(surface, WL_SURFACE_COMMIT); + } + + private static void wl_surface_destroy(IntPtr surface) + { + wl_proxy_marshal(surface, WL_SURFACE_DESTROY); + wl_proxy_destroy(surface); + } + + // wl_shm_create_pool wrapper + [DllImport(LibWaylandClient, EntryPoint = "wl_proxy_marshal_flags")] + private static extern IntPtr wl_proxy_marshal_flags_fd( + IntPtr proxy, uint opcode, IntPtr iface, uint version, uint flags, + IntPtr newId, int fd, int size); + + private static IntPtr wl_shm_create_pool(IntPtr shm, int fd, int size) + { + return wl_proxy_marshal_flags_fd(shm, WL_SHM_CREATE_POOL, + _wl_shm_pool_interface, wl_proxy_get_version(shm), 0, + IntPtr.Zero, fd, size); + } + + // wl_shm_pool methods + [DllImport(LibWaylandClient, EntryPoint = "wl_proxy_marshal_flags")] + private static extern IntPtr wl_proxy_marshal_flags_buffer( + IntPtr proxy, uint opcode, IntPtr iface, uint version, uint flags, + IntPtr newId, int offset, int width, int height, int stride, uint format); + + private static IntPtr wl_shm_pool_create_buffer(IntPtr pool, int offset, int width, int height, int stride, uint format) + { + return wl_proxy_marshal_flags_buffer(pool, WL_SHM_POOL_CREATE_BUFFER, + _wl_buffer_interface, wl_proxy_get_version(pool), 0, + IntPtr.Zero, offset, width, height, stride, format); + } + + private static void wl_shm_pool_destroy(IntPtr pool) + { + wl_proxy_marshal(pool, WL_SHM_POOL_DESTROY); + wl_proxy_destroy(pool); + } + + // wl_buffer methods + private static void wl_buffer_destroy(IntPtr buffer) + { + wl_proxy_marshal(buffer, WL_BUFFER_DESTROY); + wl_proxy_destroy(buffer); + } + + private static int wl_buffer_add_listener(IntPtr buffer, IntPtr listener, IntPtr data) + { + return wl_proxy_add_listener(buffer, listener, data); + } + + // wl_seat methods + private static int wl_seat_add_listener(IntPtr seat, IntPtr listener, IntPtr data) + { + return wl_proxy_add_listener(seat, listener, data); + } + + private static IntPtr wl_seat_get_pointer(IntPtr seat) + { + return wl_proxy_marshal_constructor(seat, WL_SEAT_GET_POINTER, + _wl_pointer_interface, IntPtr.Zero); + } + + private static IntPtr wl_seat_get_keyboard(IntPtr seat) + { + return wl_proxy_marshal_constructor(seat, WL_SEAT_GET_KEYBOARD, + _wl_keyboard_interface, IntPtr.Zero); + } + + private static int wl_pointer_add_listener(IntPtr pointer, IntPtr listener, IntPtr data) + { + return wl_proxy_add_listener(pointer, listener, data); + } + + private static int wl_keyboard_add_listener(IntPtr keyboard, IntPtr listener, IntPtr data) + { + return wl_proxy_add_listener(keyboard, listener, data); + } + + #endregion + + #region xdg-shell Protocol Wrappers + + private static IntPtr _xdg_wm_base_interface; + private static IntPtr _xdg_surface_interface; + private static IntPtr _xdg_toplevel_interface; + + // We need to create and pin interface structures for xdg-shell + private static GCHandle _xdgWmBaseInterfaceHandle; + private static GCHandle _xdgSurfaceInterfaceHandle; + private static GCHandle _xdgToplevelInterfaceHandle; + private static IntPtr _xdgWmBaseName; + private static IntPtr _xdgSurfaceName; + private static IntPtr _xdgToplevelName; + + private static void LoadXdgShellInterfaces() + { + if (_xdg_wm_base_interface != IntPtr.Zero) return; + + // xdg-shell interfaces are NOT in libwayland-client + // We need to create minimal interface structs ourselves + // The key fields are: name (string ptr), version, method_count, methods, event_count, events + + // Allocate interface names + _xdgWmBaseName = Marshal.StringToHGlobalAnsi("xdg_wm_base"); + _xdgSurfaceName = Marshal.StringToHGlobalAnsi("xdg_surface"); + _xdgToplevelName = Marshal.StringToHGlobalAnsi("xdg_toplevel"); + + // Create interface structures + var wmBaseInterface = new WlInterface + { + Name = _xdgWmBaseName, + Version = 6, + MethodCount = 4, // destroy, create_positioner, get_xdg_surface, pong + Methods = IntPtr.Zero, + EventCount = 1, // ping + Events = IntPtr.Zero + }; + _xdgWmBaseInterfaceHandle = GCHandle.Alloc(wmBaseInterface, GCHandleType.Pinned); + _xdg_wm_base_interface = _xdgWmBaseInterfaceHandle.AddrOfPinnedObject(); + + var surfaceInterface = new WlInterface + { + Name = _xdgSurfaceName, + Version = 6, + MethodCount = 5, // destroy, get_toplevel, get_popup, set_window_geometry, ack_configure + Methods = IntPtr.Zero, + EventCount = 1, // configure + Events = IntPtr.Zero + }; + _xdgSurfaceInterfaceHandle = GCHandle.Alloc(surfaceInterface, GCHandleType.Pinned); + _xdg_surface_interface = _xdgSurfaceInterfaceHandle.AddrOfPinnedObject(); + + var toplevelInterface = new WlInterface + { + Name = _xdgToplevelName, + Version = 6, + MethodCount = 14, // destroy, set_parent, set_title, set_app_id, etc. + Methods = IntPtr.Zero, + EventCount = 4, // configure, close, configure_bounds, wm_capabilities + Events = IntPtr.Zero + }; + _xdgToplevelInterfaceHandle = GCHandle.Alloc(toplevelInterface, GCHandleType.Pinned); + _xdg_toplevel_interface = _xdgToplevelInterfaceHandle.AddrOfPinnedObject(); + } + + private static IntPtr xdg_wm_base_get_xdg_surface(IntPtr wmBase, IntPtr surface) + { + return wl_proxy_marshal_constructor(wmBase, XDG_WM_BASE_GET_XDG_SURFACE, + _xdg_surface_interface, surface); + } + + private static void xdg_wm_base_pong(IntPtr wmBase, uint serial) + { + wl_proxy_marshal(wmBase, XDG_WM_BASE_PONG, serial); + } + + private static int xdg_wm_base_add_listener(IntPtr wmBase, IntPtr listener, IntPtr data) + { + return wl_proxy_add_listener(wmBase, listener, data); + } + + private static IntPtr xdg_surface_get_toplevel(IntPtr xdgSurface) + { + return wl_proxy_marshal_constructor(xdgSurface, XDG_SURFACE_GET_TOPLEVEL, + _xdg_toplevel_interface, IntPtr.Zero); + } + + private static void xdg_surface_ack_configure(IntPtr xdgSurface, uint serial) + { + wl_proxy_marshal(xdgSurface, XDG_SURFACE_ACK_CONFIGURE, serial); + } + + private static int xdg_surface_add_listener(IntPtr xdgSurface, IntPtr listener, IntPtr data) + { + return wl_proxy_add_listener(xdgSurface, listener, data); + } + + private static void xdg_surface_destroy(IntPtr xdgSurface) + { + wl_proxy_marshal(xdgSurface, XDG_SURFACE_DESTROY); + wl_proxy_destroy(xdgSurface); + } + + private static void xdg_toplevel_set_title(IntPtr toplevel, string title) + { + wl_proxy_marshal(toplevel, XDG_TOPLEVEL_SET_TITLE, title); + } + + private static void xdg_toplevel_set_app_id(IntPtr toplevel, string appId) + { + wl_proxy_marshal(toplevel, XDG_TOPLEVEL_SET_APP_ID, appId); + } + + private static int xdg_toplevel_add_listener(IntPtr toplevel, IntPtr listener, IntPtr data) + { + return wl_proxy_add_listener(toplevel, listener, data); + } + + private static void xdg_toplevel_destroy(IntPtr toplevel) + { + wl_proxy_marshal(toplevel, XDG_TOPLEVEL_DESTROY); + wl_proxy_destroy(toplevel); + } + + #endregion + + #region Native Interop - libc + + [DllImport("libc", EntryPoint = "shm_open")] + private static extern int shm_open([MarshalAs(UnmanagedType.LPStr)] string name, int oflag, int mode); + + [DllImport("libc", EntryPoint = "shm_unlink")] + private static extern int shm_unlink([MarshalAs(UnmanagedType.LPStr)] string name); + + [DllImport("libc", EntryPoint = "ftruncate")] + private static extern int ftruncate(int fd, long length); + + [DllImport("libc", EntryPoint = "mmap")] + private static extern IntPtr mmap(IntPtr addr, nuint length, int prot, int flags, int fd, long offset); + + [DllImport("libc", EntryPoint = "munmap")] + private static extern int munmap(IntPtr addr, nuint length); + + [DllImport("libc", EntryPoint = "close")] + private static extern int close(int fd); + + [DllImport("libc", EntryPoint = "memfd_create")] + private static extern int memfd_create([MarshalAs(UnmanagedType.LPStr)] string name, uint flags); + + private const int O_RDWR = 2; + private const int O_CREAT = 64; + private const int O_EXCL = 128; + private const int PROT_READ = 1; + private const int PROT_WRITE = 2; + private const int MAP_SHARED = 1; + private const uint MFD_CLOEXEC = 1; + + #endregion + + #region Wayland Structures + + [StructLayout(LayoutKind.Sequential)] + private struct WlInterface + { + public IntPtr Name; + public int Version; + public int MethodCount; + public IntPtr Methods; + public int EventCount; + public IntPtr Events; + } + + [StructLayout(LayoutKind.Sequential)] + private struct WlRegistryListener + { + public IntPtr Global; + public IntPtr GlobalRemove; + } + + [StructLayout(LayoutKind.Sequential)] + private struct WlSurfaceListener + { + public IntPtr Enter; + public IntPtr Leave; + } + + [StructLayout(LayoutKind.Sequential)] + private struct WlBufferListener + { + public IntPtr Release; + } + + [StructLayout(LayoutKind.Sequential)] + private struct WlSeatListener + { + public IntPtr Capabilities; + public IntPtr Name; + } + + [StructLayout(LayoutKind.Sequential)] + private struct WlPointerListener + { + public IntPtr Enter; + public IntPtr Leave; + public IntPtr Motion; + public IntPtr Button; + public IntPtr Axis; + public IntPtr Frame; + public IntPtr AxisSource; + public IntPtr AxisStop; + public IntPtr AxisDiscrete; + } + + [StructLayout(LayoutKind.Sequential)] + private struct WlKeyboardListener + { + public IntPtr Keymap; + public IntPtr Enter; + public IntPtr Leave; + public IntPtr Key; + public IntPtr Modifiers; + public IntPtr RepeatInfo; + } + + [StructLayout(LayoutKind.Sequential)] + private struct XdgWmBaseListener + { + public IntPtr Ping; + } + + [StructLayout(LayoutKind.Sequential)] + private struct XdgSurfaceListener + { + public IntPtr Configure; + } + + [StructLayout(LayoutKind.Sequential)] + private struct XdgToplevelListener + { + public IntPtr Configure; + public IntPtr Close; + } + + private const uint WL_SHM_FORMAT_ARGB8888 = 0; + private const uint WL_SHM_FORMAT_XRGB8888 = 1; + + // Seat capabilities + private const uint WL_SEAT_CAPABILITY_POINTER = 1; + private const uint WL_SEAT_CAPABILITY_KEYBOARD = 2; + + // Pointer button states + private const uint WL_POINTER_BUTTON_STATE_RELEASED = 0; + private const uint WL_POINTER_BUTTON_STATE_PRESSED = 1; + + // Linux input button codes + private const uint BTN_LEFT = 0x110; + private const uint BTN_RIGHT = 0x111; + private const uint BTN_MIDDLE = 0x112; + + // Key states + private const uint WL_KEYBOARD_KEY_STATE_RELEASED = 0; + private const uint WL_KEYBOARD_KEY_STATE_PRESSED = 1; + + #endregion + + #region Fields + + private IntPtr _display; + private IntPtr _registry; + private IntPtr _compositor; + private IntPtr _shm; + private IntPtr _seat; + private IntPtr _xdgWmBase; + private IntPtr _surface; + private IntPtr _xdgSurface; + private IntPtr _xdgToplevel; + private IntPtr _pointer; + private IntPtr _keyboard; + private IntPtr _shmPool; + private IntPtr _buffer; + private IntPtr _pixelData; + private int _shmFd = -1; + private int _bufferSize; + private int _stride; + + private int _width; + private int _height; + private int _pendingWidth; + private int _pendingHeight; + private string _title; + private bool _isRunning; + private bool _disposed; + private bool _configured; + private uint _lastConfigureSerial; + + // Input state + private float _pointerX; + private float _pointerY; + private uint _pointerSerial; + private uint _modifiers; + + // Delegates to prevent GC + private WlRegistryListener _registryListener; + private WlSeatListener _seatListener; + private WlPointerListener _pointerListener; + private WlKeyboardListener _keyboardListener; + private XdgWmBaseListener _wmBaseListener; + private XdgSurfaceListener _xdgSurfaceListener; + private XdgToplevelListener _toplevelListener; + private WlBufferListener _bufferListener; + + // GCHandles for listener structs to prevent GC + private GCHandle _registryListenerHandle; + private GCHandle _seatListenerHandle; + private GCHandle _pointerListenerHandle; + private GCHandle _keyboardListenerHandle; + private GCHandle _wmBaseListenerHandle; + private GCHandle _xdgSurfaceListenerHandle; + private GCHandle _toplevelListenerHandle; + private GCHandle _bufferListenerHandle; + + private static bool _interfacesInitialized; + + // GCHandles to prevent delegate collection + private GCHandle _thisHandle; + + #endregion + + #region Properties + + public IntPtr Display => _display; + public IntPtr Surface => _surface; + public int Width => _width; + public int Height => _height; + public bool IsRunning => _isRunning; + public IntPtr PixelData => _pixelData; + public int Stride => _stride; + + #endregion + + #region Events + + public event EventHandler? KeyDown; + public event EventHandler? KeyUp; + public event EventHandler? TextInput; + public event EventHandler? PointerMoved; + public event EventHandler? PointerPressed; + public event EventHandler? PointerReleased; + public event EventHandler? Scroll; + public event EventHandler? Exposed; + public event EventHandler<(int Width, int Height)>? Resized; + public event EventHandler? CloseRequested; + public event EventHandler? FocusGained; + public event EventHandler? FocusLost; + + #endregion + + #region Constructor + + public WaylandWindow(string title, int width, int height) + { + _title = title; + _width = width; + _height = height; + _pendingWidth = width; + _pendingHeight = height; + + InitializeInterfaces(); + Initialize(); + } + + #endregion + + #region Initialization + + private static void InitializeInterfaces() + { + if (_interfacesInitialized) return; + + // Load interface symbols from libwayland-client using dlsym + LoadInterfaceSymbols(); + LoadXdgShellInterfaces(); + + _interfacesInitialized = true; + } + + private void Initialize() + { + // Keep this object alive for callbacks + _thisHandle = GCHandle.Alloc(this); + + // Connect to Wayland display + _display = wl_display_connect(null); + if (_display == IntPtr.Zero) + { + throw new InvalidOperationException( + "Failed to connect to Wayland display. " + + "Ensure WAYLAND_DISPLAY is set and a compositor is running."); + } + + // Get registry + _registry = wl_display_get_registry(_display); + if (_registry == IntPtr.Zero) + { + throw new InvalidOperationException("Failed to get Wayland registry"); + } + + // Set up registry listener + _registryListener = new WlRegistryListener + { + Global = Marshal.GetFunctionPointerForDelegate(RegistryGlobal), + GlobalRemove = Marshal.GetFunctionPointerForDelegate(RegistryGlobalRemove) + }; + _registryListenerHandle = GCHandle.Alloc(_registryListener, GCHandleType.Pinned); + wl_registry_add_listener(_registry, _registryListenerHandle.AddrOfPinnedObject(), GCHandle.ToIntPtr(_thisHandle)); + + // Do initial roundtrip to get globals + wl_display_roundtrip(_display); + + // Verify we got required globals + if (_compositor == IntPtr.Zero) + throw new InvalidOperationException("Wayland compositor not found"); + if (_shm == IntPtr.Zero) + throw new InvalidOperationException("Wayland shm not found"); + if (_xdgWmBase == IntPtr.Zero) + throw new InvalidOperationException("xdg_wm_base not found - compositor doesn't support xdg-shell"); + + // Create surface + _surface = wl_compositor_create_surface(_compositor); + if (_surface == IntPtr.Zero) + throw new InvalidOperationException("Failed to create Wayland surface"); + + // Create xdg_surface + _xdgSurface = xdg_wm_base_get_xdg_surface(_xdgWmBase, _surface); + if (_xdgSurface == IntPtr.Zero) + throw new InvalidOperationException("Failed to create xdg_surface"); + + _xdgSurfaceListener = new XdgSurfaceListener + { + Configure = Marshal.GetFunctionPointerForDelegate(XdgSurfaceConfigure) + }; + _xdgSurfaceListenerHandle = GCHandle.Alloc(_xdgSurfaceListener, GCHandleType.Pinned); + xdg_surface_add_listener(_xdgSurface, _xdgSurfaceListenerHandle.AddrOfPinnedObject(), GCHandle.ToIntPtr(_thisHandle)); + + // Create toplevel + _xdgToplevel = xdg_surface_get_toplevel(_xdgSurface); + if (_xdgToplevel == IntPtr.Zero) + throw new InvalidOperationException("Failed to create xdg_toplevel"); + + _toplevelListener = new XdgToplevelListener + { + Configure = Marshal.GetFunctionPointerForDelegate(XdgToplevelConfigure), + Close = Marshal.GetFunctionPointerForDelegate(XdgToplevelClose) + }; + _toplevelListenerHandle = GCHandle.Alloc(_toplevelListener, GCHandleType.Pinned); + xdg_toplevel_add_listener(_xdgToplevel, _toplevelListenerHandle.AddrOfPinnedObject(), GCHandle.ToIntPtr(_thisHandle)); + + // Set title and app_id + xdg_toplevel_set_title(_xdgToplevel, _title); + xdg_toplevel_set_app_id(_xdgToplevel, "com.openmaui.app"); + + // Commit empty surface to get initial configure + wl_surface_commit(_surface); + wl_display_roundtrip(_display); + + // Create shared memory buffer + CreateShmBuffer(); + + Console.WriteLine($"[Wayland] Window created: {_width}x{_height}"); + } + + private void CreateShmBuffer() + { + _stride = _width * 4; + _bufferSize = _stride * _height; + + // Create anonymous file for shared memory + _shmFd = memfd_create("maui-buffer", MFD_CLOEXEC); + if (_shmFd < 0) + { + // Fall back to shm_open + string shmName = $"/maui-{Environment.ProcessId}-{DateTime.Now.Ticks}"; + _shmFd = shm_open(shmName, O_RDWR | O_CREAT | O_EXCL, 0x180); // 0600 + if (_shmFd >= 0) + shm_unlink(shmName); + } + + if (_shmFd < 0) + throw new InvalidOperationException("Failed to create shared memory"); + + if (ftruncate(_shmFd, _bufferSize) < 0) + { + close(_shmFd); + throw new InvalidOperationException("Failed to resize shared memory"); + } + + _pixelData = mmap(IntPtr.Zero, (nuint)_bufferSize, PROT_READ | PROT_WRITE, MAP_SHARED, _shmFd, 0); + if (_pixelData == IntPtr.Zero || _pixelData == new IntPtr(-1)) + { + close(_shmFd); + throw new InvalidOperationException("Failed to mmap shared memory"); + } + + // Create pool and buffer + _shmPool = wl_shm_create_pool(_shm, _shmFd, _bufferSize); + if (_shmPool == IntPtr.Zero) + { + munmap(_pixelData, (nuint)_bufferSize); + close(_shmFd); + throw new InvalidOperationException("Failed to create wl_shm_pool"); + } + + _buffer = wl_shm_pool_create_buffer(_shmPool, 0, _width, _height, _stride, WL_SHM_FORMAT_ARGB8888); + if (_buffer == IntPtr.Zero) + { + wl_shm_pool_destroy(_shmPool); + munmap(_pixelData, (nuint)_bufferSize); + close(_shmFd); + throw new InvalidOperationException("Failed to create wl_buffer"); + } + + // Listen for buffer release + _bufferListener = new WlBufferListener + { + Release = Marshal.GetFunctionPointerForDelegate(BufferRelease) + }; + if (_bufferListenerHandle.IsAllocated) _bufferListenerHandle.Free(); + _bufferListenerHandle = GCHandle.Alloc(_bufferListener, GCHandleType.Pinned); + wl_buffer_add_listener(_buffer, _bufferListenerHandle.AddrOfPinnedObject(), GCHandle.ToIntPtr(_thisHandle)); + } + + private void ResizeBuffer(int newWidth, int newHeight) + { + if (newWidth == _width && newHeight == _height) return; + if (newWidth <= 0 || newHeight <= 0) return; + + // Destroy old buffer + if (_buffer != IntPtr.Zero) + wl_buffer_destroy(_buffer); + if (_shmPool != IntPtr.Zero) + wl_shm_pool_destroy(_shmPool); + if (_pixelData != IntPtr.Zero && _pixelData != new IntPtr(-1)) + munmap(_pixelData, (nuint)_bufferSize); + if (_shmFd >= 0) + close(_shmFd); + + _width = newWidth; + _height = newHeight; + + CreateShmBuffer(); + Resized?.Invoke(this, (_width, _height)); + } + + #endregion + + #region Callback Delegates + + private delegate void RegistryGlobalDelegate(IntPtr data, IntPtr registry, uint name, IntPtr iface, uint version); + private delegate void RegistryGlobalRemoveDelegate(IntPtr data, IntPtr registry, uint name); + private delegate void SeatCapabilitiesDelegate(IntPtr data, IntPtr seat, uint capabilities); + private delegate void SeatNameDelegate(IntPtr data, IntPtr seat, IntPtr name); + private delegate void PointerEnterDelegate(IntPtr data, IntPtr pointer, uint serial, IntPtr surface, int x, int y); + private delegate void PointerLeaveDelegate(IntPtr data, IntPtr pointer, uint serial, IntPtr surface); + private delegate void PointerMotionDelegate(IntPtr data, IntPtr pointer, uint time, int x, int y); + private delegate void PointerButtonDelegate(IntPtr data, IntPtr pointer, uint serial, uint time, uint button, uint state); + private delegate void PointerAxisDelegate(IntPtr data, IntPtr pointer, uint time, uint axis, int value); + private delegate void PointerFrameDelegate(IntPtr data, IntPtr pointer); + private delegate void KeyboardKeymapDelegate(IntPtr data, IntPtr keyboard, uint format, int fd, uint size); + private delegate void KeyboardEnterDelegate(IntPtr data, IntPtr keyboard, uint serial, IntPtr surface, IntPtr keys); + private delegate void KeyboardLeaveDelegate(IntPtr data, IntPtr keyboard, uint serial, IntPtr surface); + private delegate void KeyboardKeyDelegate(IntPtr data, IntPtr keyboard, uint serial, uint time, uint key, uint state); + private delegate void KeyboardModifiersDelegate(IntPtr data, IntPtr keyboard, uint serial, uint modsDepressed, uint modsLatched, uint modsLocked, uint group); + private delegate void XdgWmBasePingDelegate(IntPtr data, IntPtr wmBase, uint serial); + private delegate void XdgSurfaceConfigureDelegate(IntPtr data, IntPtr xdgSurface, uint serial); + private delegate void XdgToplevelConfigureDelegate(IntPtr data, IntPtr toplevel, int width, int height, IntPtr states); + private delegate void XdgToplevelCloseDelegate(IntPtr data, IntPtr toplevel); + private delegate void BufferReleaseDelegate(IntPtr data, IntPtr buffer); + + #endregion + + #region Callback Implementations + + private static void RegistryGlobal(IntPtr data, IntPtr registry, uint name, IntPtr iface, uint version) + { + var handle = GCHandle.FromIntPtr(data); + if (!handle.IsAllocated) return; + var window = (WaylandWindow)handle.Target!; + + var interfaceName = Marshal.PtrToStringAnsi(iface); + Console.WriteLine($"[Wayland] Global: {interfaceName} v{version}"); + + switch (interfaceName) + { + case "wl_compositor": + window._compositor = wl_registry_bind(registry, name, _wl_compositor_interface, Math.Min(version, 4u)); + break; + case "wl_shm": + window._shm = wl_registry_bind(registry, name, _wl_shm_interface, 1); + break; + case "wl_seat": + window._seat = wl_registry_bind(registry, name, _wl_seat_interface, Math.Min(version, 5u)); + window.SetupSeat(); + break; + case "xdg_wm_base": + window._xdgWmBase = wl_registry_bind(registry, name, _xdg_wm_base_interface, Math.Min(version, 2u)); + window.SetupXdgWmBase(); + break; + } + } + + private static void RegistryGlobalRemove(IntPtr data, IntPtr registry, uint name) + { + // Handle global removal if needed + } + + private void SetupSeat() + { + if (_seat == IntPtr.Zero) return; + + _seatListener = new WlSeatListener + { + Capabilities = Marshal.GetFunctionPointerForDelegate(SeatCapabilities), + Name = Marshal.GetFunctionPointerForDelegate(SeatName) + }; + _seatListenerHandle = GCHandle.Alloc(_seatListener, GCHandleType.Pinned); + wl_seat_add_listener(_seat, _seatListenerHandle.AddrOfPinnedObject(), GCHandle.ToIntPtr(_thisHandle)); + } + + private static void SeatCapabilities(IntPtr data, IntPtr seat, uint capabilities) + { + var handle = GCHandle.FromIntPtr(data); + if (!handle.IsAllocated) return; + var window = (WaylandWindow)handle.Target!; + + if ((capabilities & WL_SEAT_CAPABILITY_POINTER) != 0 && window._pointer == IntPtr.Zero) + { + window._pointer = wl_seat_get_pointer(seat); + window.SetupPointer(); + } + + if ((capabilities & WL_SEAT_CAPABILITY_KEYBOARD) != 0 && window._keyboard == IntPtr.Zero) + { + window._keyboard = wl_seat_get_keyboard(seat); + window.SetupKeyboard(); + } + } + + private static void SeatName(IntPtr data, IntPtr seat, IntPtr name) { } + + private void SetupPointer() + { + if (_pointer == IntPtr.Zero) return; + + _pointerListener = new WlPointerListener + { + Enter = Marshal.GetFunctionPointerForDelegate(PointerEnter), + Leave = Marshal.GetFunctionPointerForDelegate(PointerLeave), + Motion = Marshal.GetFunctionPointerForDelegate(PointerMotion), + Button = Marshal.GetFunctionPointerForDelegate(OnPointerButton), + Axis = Marshal.GetFunctionPointerForDelegate(PointerAxis), + Frame = Marshal.GetFunctionPointerForDelegate(PointerFrame), + }; + _pointerListenerHandle = GCHandle.Alloc(_pointerListener, GCHandleType.Pinned); + wl_pointer_add_listener(_pointer, _pointerListenerHandle.AddrOfPinnedObject(), GCHandle.ToIntPtr(_thisHandle)); + } + + private static void PointerEnter(IntPtr data, IntPtr pointer, uint serial, IntPtr surface, int x, int y) + { + var handle = GCHandle.FromIntPtr(data); + if (!handle.IsAllocated) return; + var window = (WaylandWindow)handle.Target!; + window._pointerSerial = serial; + window._pointerX = x / 256.0f; + window._pointerY = y / 256.0f; + } + + private static void PointerLeave(IntPtr data, IntPtr pointer, uint serial, IntPtr surface) { } + + private static void PointerMotion(IntPtr data, IntPtr pointer, uint time, int x, int y) + { + var handle = GCHandle.FromIntPtr(data); + if (!handle.IsAllocated) return; + var window = (WaylandWindow)handle.Target!; + + window._pointerX = x / 256.0f; + window._pointerY = y / 256.0f; + window.PointerMoved?.Invoke(window, new PointerEventArgs((int)window._pointerX, (int)window._pointerY)); + } + + private static void OnPointerButton(IntPtr data, IntPtr pointer, uint serial, uint time, uint button, uint state) + { + var handle = GCHandle.FromIntPtr(data); + if (!handle.IsAllocated) return; + var window = (WaylandWindow)handle.Target!; + + var ptrButton = button switch + { + BTN_LEFT => Microsoft.Maui.Platform.PointerButton.Left, + BTN_RIGHT => Microsoft.Maui.Platform.PointerButton.Right, + BTN_MIDDLE => Microsoft.Maui.Platform.PointerButton.Middle, + _ => Microsoft.Maui.Platform.PointerButton.None + }; + + var args = new PointerEventArgs((int)window._pointerX, (int)window._pointerY, ptrButton); + + if (state == WL_POINTER_BUTTON_STATE_PRESSED) + window.PointerPressed?.Invoke(window, args); + else + window.PointerReleased?.Invoke(window, args); + } + + private static void PointerAxis(IntPtr data, IntPtr pointer, uint time, uint axis, int value) + { + var handle = GCHandle.FromIntPtr(data); + if (!handle.IsAllocated) return; + var window = (WaylandWindow)handle.Target!; + + float delta = value / 256.0f / 10.0f; + if (axis == 0) // Vertical + window.Scroll?.Invoke(window, new ScrollEventArgs((int)window._pointerX, (int)window._pointerY, 0, delta)); + else // Horizontal + window.Scroll?.Invoke(window, new ScrollEventArgs((int)window._pointerX, (int)window._pointerY, delta, 0)); + } + + private static void PointerFrame(IntPtr data, IntPtr pointer) { } + + private void SetupKeyboard() + { + if (_keyboard == IntPtr.Zero) return; + + _keyboardListener = new WlKeyboardListener + { + Keymap = Marshal.GetFunctionPointerForDelegate(KeyboardKeymap), + Enter = Marshal.GetFunctionPointerForDelegate(KeyboardEnter), + Leave = Marshal.GetFunctionPointerForDelegate(KeyboardLeave), + Key = Marshal.GetFunctionPointerForDelegate(KeyboardKey), + Modifiers = Marshal.GetFunctionPointerForDelegate(KeyboardModifiers), + }; + _keyboardListenerHandle = GCHandle.Alloc(_keyboardListener, GCHandleType.Pinned); + wl_keyboard_add_listener(_keyboard, _keyboardListenerHandle.AddrOfPinnedObject(), GCHandle.ToIntPtr(_thisHandle)); + } + + private static void KeyboardKeymap(IntPtr data, IntPtr keyboard, uint format, int fd, uint size) + { + close(fd); + } + + private static void KeyboardEnter(IntPtr data, IntPtr keyboard, uint serial, IntPtr surface, IntPtr keys) + { + var handle = GCHandle.FromIntPtr(data); + if (!handle.IsAllocated) return; + var window = (WaylandWindow)handle.Target!; + window.FocusGained?.Invoke(window, EventArgs.Empty); + } + + private static void KeyboardLeave(IntPtr data, IntPtr keyboard, uint serial, IntPtr surface) + { + var handle = GCHandle.FromIntPtr(data); + if (!handle.IsAllocated) return; + var window = (WaylandWindow)handle.Target!; + window.FocusLost?.Invoke(window, EventArgs.Empty); + } + + private static void KeyboardKey(IntPtr data, IntPtr keyboard, uint serial, uint time, uint keycode, uint state) + { + var handle = GCHandle.FromIntPtr(data); + if (!handle.IsAllocated) return; + var window = (WaylandWindow)handle.Target!; + + // Convert Linux keycode to Key enum (add 8 for X11 compat) + var key = KeyMapping.FromLinuxKeycode(keycode + 8); + var modifiers = (KeyModifiers)window._modifiers; + var args = new KeyEventArgs(key, modifiers); + + if (state == WL_KEYBOARD_KEY_STATE_PRESSED) + { + window.KeyDown?.Invoke(window, args); + + // Generate text input for printable keys + char? ch = KeyMapping.ToChar(key, modifiers); + if (ch.HasValue) + window.TextInput?.Invoke(window, new TextInputEventArgs(ch.Value.ToString())); + } + else + { + window.KeyUp?.Invoke(window, args); + } + } + + private static void KeyboardModifiers(IntPtr data, IntPtr keyboard, uint serial, uint modsDepressed, uint modsLatched, uint modsLocked, uint group) + { + var handle = GCHandle.FromIntPtr(data); + if (!handle.IsAllocated) return; + var window = (WaylandWindow)handle.Target!; + window._modifiers = modsDepressed | modsLatched; + } + + private void SetupXdgWmBase() + { + if (_xdgWmBase == IntPtr.Zero) return; + + _wmBaseListener = new XdgWmBaseListener + { + Ping = Marshal.GetFunctionPointerForDelegate(XdgWmBasePing) + }; + _wmBaseListenerHandle = GCHandle.Alloc(_wmBaseListener, GCHandleType.Pinned); + xdg_wm_base_add_listener(_xdgWmBase, _wmBaseListenerHandle.AddrOfPinnedObject(), GCHandle.ToIntPtr(_thisHandle)); + } + + private static void XdgWmBasePing(IntPtr data, IntPtr wmBase, uint serial) + { + xdg_wm_base_pong(wmBase, serial); + } + + private static void XdgSurfaceConfigure(IntPtr data, IntPtr xdgSurface, uint serial) + { + var handle = GCHandle.FromIntPtr(data); + if (!handle.IsAllocated) return; + var window = (WaylandWindow)handle.Target!; + + xdg_surface_ack_configure(xdgSurface, serial); + window._lastConfigureSerial = serial; + + if (!window._configured) + { + window._configured = true; + if (window._pendingWidth > 0 && window._pendingHeight > 0) + { + window.ResizeBuffer(window._pendingWidth, window._pendingHeight); + } + window.Exposed?.Invoke(window, EventArgs.Empty); + } + } + + private static void XdgToplevelConfigure(IntPtr data, IntPtr toplevel, int width, int height, IntPtr states) + { + var handle = GCHandle.FromIntPtr(data); + if (!handle.IsAllocated) return; + var window = (WaylandWindow)handle.Target!; + + if (width > 0 && height > 0) + { + window._pendingWidth = width; + window._pendingHeight = height; + + if (window._configured) + { + window.ResizeBuffer(width, height); + } + } + } + + private static void XdgToplevelClose(IntPtr data, IntPtr toplevel) + { + var handle = GCHandle.FromIntPtr(data); + if (!handle.IsAllocated) return; + var window = (WaylandWindow)handle.Target!; + + window.CloseRequested?.Invoke(window, EventArgs.Empty); + window._isRunning = false; + } + + private static void BufferRelease(IntPtr data, IntPtr buffer) + { + // Buffer is available for reuse + } + + #endregion + + #region Public Methods + + public void Show() + { + _isRunning = true; + + // Attach buffer and commit + wl_surface_attach(_surface, _buffer, 0, 0); + wl_surface_damage_buffer(_surface, 0, 0, _width, _height); + wl_surface_commit(_surface); + wl_display_flush(_display); + } + + public void Hide() + { + wl_surface_attach(_surface, IntPtr.Zero, 0, 0); + wl_surface_commit(_surface); + wl_display_flush(_display); + } + + public void SetTitle(string title) + { + _title = title; + if (_xdgToplevel != IntPtr.Zero) + xdg_toplevel_set_title(_xdgToplevel, title); + } + + public void Resize(int width, int height) + { + ResizeBuffer(width, height); + } + + public void ProcessEvents() + { + if (!_isRunning || _display == IntPtr.Zero) return; + + wl_display_dispatch_pending(_display); + wl_display_flush(_display); + } + + public void Stop() + { + _isRunning = false; + } + + public void CommitFrame() + { + if (_surface != IntPtr.Zero && _buffer != IntPtr.Zero) + { + wl_surface_attach(_surface, _buffer, 0, 0); + wl_surface_damage_buffer(_surface, 0, 0, _width, _height); + wl_surface_commit(_surface); + wl_display_flush(_display); + } + } + + public int GetFileDescriptor() + { + return wl_display_get_fd(_display); + } + + #endregion + + #region IDisposable + + public void Dispose() + { + if (_disposed) return; + _disposed = true; + _isRunning = false; + + if (_buffer != IntPtr.Zero) + { + wl_buffer_destroy(_buffer); + _buffer = IntPtr.Zero; + } + + if (_shmPool != IntPtr.Zero) + { + wl_shm_pool_destroy(_shmPool); + _shmPool = IntPtr.Zero; + } + + if (_pixelData != IntPtr.Zero && _pixelData != new IntPtr(-1)) + { + munmap(_pixelData, (nuint)_bufferSize); + _pixelData = IntPtr.Zero; + } + + if (_shmFd >= 0) + { + close(_shmFd); + _shmFd = -1; + } + + if (_xdgToplevel != IntPtr.Zero) + { + xdg_toplevel_destroy(_xdgToplevel); + _xdgToplevel = IntPtr.Zero; + } + + if (_xdgSurface != IntPtr.Zero) + { + xdg_surface_destroy(_xdgSurface); + _xdgSurface = IntPtr.Zero; + } + + if (_surface != IntPtr.Zero) + { + wl_surface_destroy(_surface); + _surface = IntPtr.Zero; + } + + if (_display != IntPtr.Zero) + { + wl_display_disconnect(_display); + _display = IntPtr.Zero; + } + + // Free listener GCHandles + if (_registryListenerHandle.IsAllocated) _registryListenerHandle.Free(); + if (_seatListenerHandle.IsAllocated) _seatListenerHandle.Free(); + if (_pointerListenerHandle.IsAllocated) _pointerListenerHandle.Free(); + if (_keyboardListenerHandle.IsAllocated) _keyboardListenerHandle.Free(); + if (_wmBaseListenerHandle.IsAllocated) _wmBaseListenerHandle.Free(); + if (_xdgSurfaceListenerHandle.IsAllocated) _xdgSurfaceListenerHandle.Free(); + if (_toplevelListenerHandle.IsAllocated) _toplevelListenerHandle.Free(); + if (_bufferListenerHandle.IsAllocated) _bufferListenerHandle.Free(); + + if (_thisHandle.IsAllocated) + _thisHandle.Free(); + + GC.SuppressFinalize(this); + } + + ~WaylandWindow() + { + Dispose(); + } + + #endregion } diff --git a/Window/X11Window.cs b/Window/X11Window.cs index 79e3c3a..49358fa 100644 --- a/Window/X11Window.cs +++ b/Window/X11Window.cs @@ -1,494 +1,464 @@ -using System; -using System.IO; -using System.Runtime.InteropServices; -using Microsoft.Maui.Platform.Linux.Input; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using Microsoft.Maui.Platform.Linux.Interop; -using SkiaSharp; -using Svg.Skia; +using Microsoft.Maui.Platform.Linux.Input; namespace Microsoft.Maui.Platform.Linux.Window; +/// +/// X11 window implementation for Linux. +/// public class X11Window : IDisposable { - private IntPtr _display; + private IntPtr _display; + private IntPtr _window; + private IntPtr _wmDeleteMessage; + private int _screen; + private bool _disposed; + private bool _isRunning; - private IntPtr _window; + private int _width; + private int _height; - private IntPtr _wmDeleteMessage; + /// + /// Gets the native display handle. + /// + public IntPtr Display => _display; - private int _screen; + /// + /// Gets the native window handle. + /// + public IntPtr Handle => _window; - private bool _disposed; + /// + /// Gets the window width. + /// + public int Width => _width; - private bool _isRunning; + /// + /// Gets the window height. + /// + public int Height => _height; - private int _width; + /// + /// Gets whether the window is running. + /// + public bool IsRunning => _isRunning; - private int _height; + /// + /// Event raised when a key is pressed. + /// + public event EventHandler? KeyDown; - private IntPtr _arrowCursor; + /// + /// Event raised when a key is released. + /// + public event EventHandler? KeyUp; - private IntPtr _handCursor; + /// + /// Event raised when text is input. + /// + public event EventHandler? TextInput; - private IntPtr _textCursor; + /// + /// Event raised when the pointer moves. + /// + public event EventHandler? PointerMoved; - private IntPtr _currentCursor; + /// + /// Event raised when a pointer button is pressed. + /// + public event EventHandler? PointerPressed; - private CursorType _currentCursorType; + /// + /// Event raised when a pointer button is released. + /// + public event EventHandler? PointerReleased; - private static int _eventCounter; + /// + /// Event raised when the mouse wheel is scrolled. + /// + public event EventHandler? Scroll; - public IntPtr Display => _display; + /// + /// Event raised when the window needs to be redrawn. + /// + public event EventHandler? Exposed; - public IntPtr Handle => _window; + /// + /// Event raised when the window is resized. + /// + public event EventHandler<(int Width, int Height)>? Resized; - public int Width => _width; + /// + /// Event raised when the window close is requested. + /// + public event EventHandler? CloseRequested; - public int Height => _height; + /// + /// Event raised when the window gains focus. + /// + public event EventHandler? FocusGained; - public bool IsRunning => _isRunning; + /// + /// Event raised when the window loses focus. + /// + public event EventHandler? FocusLost; - public event EventHandler? KeyDown; + /// + /// Creates a new X11 window. + /// + public X11Window(string title, int width, int height) + { + _width = width; + _height = height; - public event EventHandler? KeyUp; + // Open display + _display = X11.XOpenDisplay(IntPtr.Zero); + if (_display == IntPtr.Zero) + throw new InvalidOperationException("Failed to open X11 display. Is X11 running?"); - public event EventHandler? TextInput; + _screen = X11.XDefaultScreen(_display); + var rootWindow = X11.XRootWindow(_display, _screen); - public event EventHandler? PointerMoved; + // Create window + _window = X11.XCreateSimpleWindow( + _display, + rootWindow, + 0, 0, + (uint)width, (uint)height, + 0, + 0, + 0xFFFFFF // White background + ); - public event EventHandler? PointerPressed; + if (_window == IntPtr.Zero) + throw new InvalidOperationException("Failed to create X11 window"); - public event EventHandler? PointerReleased; + // Set window title + X11.XStoreName(_display, _window, title); - public event EventHandler? Scroll; + // Select input events + X11.XSelectInput(_display, _window, + XEventMask.KeyPressMask | + XEventMask.KeyReleaseMask | + XEventMask.ButtonPressMask | + XEventMask.ButtonReleaseMask | + XEventMask.PointerMotionMask | + XEventMask.EnterWindowMask | + XEventMask.LeaveWindowMask | + XEventMask.ExposureMask | + XEventMask.StructureNotifyMask | + XEventMask.FocusChangeMask); - public event EventHandler? Exposed; + // Set up WM_DELETE_WINDOW protocol for proper close handling + _wmDeleteMessage = X11.XInternAtom(_display, "WM_DELETE_WINDOW", false); - public event EventHandler<(int Width, int Height)>? Resized; + // Would need XSetWMProtocols here, simplified for now + } - public event EventHandler? CloseRequested; + /// + /// Shows the window. + /// + public void Show() + { + X11.XMapWindow(_display, _window); + X11.XFlush(_display); + _isRunning = true; + } - public event EventHandler? FocusGained; + /// + /// Hides the window. + /// + public void Hide() + { + X11.XUnmapWindow(_display, _window); + X11.XFlush(_display); + } - public event EventHandler? FocusLost; + /// + /// Sets the window title. + /// + public void SetTitle(string title) + { + X11.XStoreName(_display, _window, title); + } - public X11Window(string title, int width, int height) - { - _width = width; - _height = height; - _display = X11.XOpenDisplay(IntPtr.Zero); - if (_display == IntPtr.Zero) - { - throw new InvalidOperationException("Failed to open X11 display. Is X11 running?"); - } - _screen = X11.XDefaultScreen(_display); - IntPtr parent = X11.XRootWindow(_display, _screen); - _window = X11.XCreateSimpleWindow(_display, parent, 0, 0, (uint)width, (uint)height, 0u, 0uL, 16777215uL); - if (_window == IntPtr.Zero) - { - throw new InvalidOperationException("Failed to create X11 window"); - } - X11.XStoreName(_display, _window, title); - long num = 2261119L; - Console.WriteLine($"[X11Window] Setting event mask: {num} (0x{num:X})"); - X11.XSelectInput(_display, _window, num); - _wmDeleteMessage = X11.XInternAtom(_display, "WM_DELETE_WINDOW", onlyIfExists: false); - _arrowCursor = X11.XCreateFontCursor(_display, 68u); - _handCursor = X11.XCreateFontCursor(_display, 60u); - _textCursor = X11.XCreateFontCursor(_display, 152u); - _currentCursor = _arrowCursor; - } + /// + /// Resizes the window. + /// + public void Resize(int width, int height) + { + X11.XResizeWindow(_display, _window, (uint)width, (uint)height); + X11.XFlush(_display); + } - public void SetCursor(CursorType cursorType) - { - if (_currentCursorType != cursorType) - { - _currentCursorType = cursorType; - IntPtr intPtr = cursorType switch - { - CursorType.Hand => _handCursor, - CursorType.Text => _textCursor, - _ => _arrowCursor, - }; - if (intPtr != _currentCursor) - { - _currentCursor = intPtr; - X11.XDefineCursor(_display, _window, _currentCursor); - X11.XFlush(_display); - } - } - } + /// + /// Processes pending X11 events. + /// + public void ProcessEvents() + { + while (X11.XPending(_display) > 0) + { + X11.XNextEvent(_display, out var xEvent); + HandleEvent(ref xEvent); + } + } - public void Show() - { - X11.XMapWindow(_display, _window); - X11.XFlush(_display); - _isRunning = true; - } + /// + /// Runs the event loop. + /// + public void Run() + { + _isRunning = true; + while (_isRunning) + { + X11.XNextEvent(_display, out var xEvent); + HandleEvent(ref xEvent); + } + } - public void Hide() - { - X11.XUnmapWindow(_display, _window); - X11.XFlush(_display); - } + /// + /// Stops the event loop. + /// + public void Stop() + { + _isRunning = false; + } - public void SetTitle(string title) - { - X11.XStoreName(_display, _window, title); - } + private void HandleEvent(ref XEvent xEvent) + { + switch (xEvent.Type) + { + case XEventType.KeyPress: + HandleKeyPress(ref xEvent.KeyEvent); + break; - public void Resize(int width, int height) - { - X11.XResizeWindow(_display, _window, (uint)width, (uint)height); - X11.XFlush(_display); - } + case XEventType.KeyRelease: + HandleKeyRelease(ref xEvent.KeyEvent); + break; - public unsafe void SetIcon(string iconPath) - { - //IL_004e: Unknown result type (might be due to invalid IL or missing references) - //IL_0055: Expected O, but got Unknown - //IL_0071: Unknown result type (might be due to invalid IL or missing references) - //IL_0076: Unknown result type (might be due to invalid IL or missing references) - //IL_00ac: Unknown result type (might be due to invalid IL or missing references) - //IL_00b2: Expected O, but got Unknown - //IL_00b3: Unknown result type (might be due to invalid IL or missing references) - //IL_00ba: Expected O, but got Unknown - //IL_0183: Unknown result type (might be due to invalid IL or missing references) - //IL_018a: Expected O, but got Unknown - //IL_00bc: Unknown result type (might be due to invalid IL or missing references) - //IL_01d4: Unknown result type (might be due to invalid IL or missing references) - //IL_01d9: Unknown result type (might be due to invalid IL or missing references) - if (string.IsNullOrEmpty(iconPath) || !File.Exists(iconPath)) - { - Console.WriteLine("[X11Window] Icon file not found: " + iconPath); - return; - } - Console.WriteLine("[X11Window] SetIcon called: " + iconPath); - try - { - SKBitmap val = null; - if (iconPath.EndsWith(".svg", StringComparison.OrdinalIgnoreCase)) - { - Console.WriteLine("[X11Window] Loading SVG icon"); - SKSvg val2 = new SKSvg(); - try - { - val2.Load(iconPath); - if (val2.Picture != null) - { - SKRect cullRect = val2.Picture.CullRect; - float num = 48f / Math.Max(((SKRect)(ref cullRect)).Width, ((SKRect)(ref cullRect)).Height); - int num2 = (int)(((SKRect)(ref cullRect)).Width * num); - int num3 = (int)(((SKRect)(ref cullRect)).Height * num); - val = new SKBitmap(num2, num3, false); - SKCanvas val3 = new SKCanvas(val); - try - { - val3.Clear(SKColors.Transparent); - val3.Scale(num); - val3.DrawPicture(val2.Picture, (SKPaint)null); - } - finally - { - ((IDisposable)val3)?.Dispose(); - } - } - } - finally - { - ((IDisposable)val2)?.Dispose(); - } - } - else - { - Console.WriteLine("[X11Window] Loading raster icon"); - val = SKBitmap.Decode(iconPath); - } - if (val == null) - { - Console.WriteLine("[X11Window] Failed to load icon: " + iconPath); - return; - } - Console.WriteLine($"[X11Window] Loaded bitmap: {val.Width}x{val.Height}"); - int num4 = 64; - if (val.Width != num4 || val.Height != num4) - { - SKBitmap val4 = new SKBitmap(num4, num4, false); - val.ScalePixels(val4, (SKFilterQuality)3); - ((SKNativeObject)val).Dispose(); - val = val4; - } - int width = val.Width; - int height = val.Height; - int num5 = 2 + width * height; - uint[] array = new uint[num5]; - array[0] = (uint)width; - array[1] = (uint)height; - for (int i = 0; i < height; i++) - { - for (int j = 0; j < width; j++) - { - SKColor pixel = val.GetPixel(j, i); - array[2 + i * width + j] = (uint)((((SKColor)(ref pixel)).Alpha << 24) | (((SKColor)(ref pixel)).Red << 16) | (((SKColor)(ref pixel)).Green << 8) | ((SKColor)(ref pixel)).Blue); - } - } - ((SKNativeObject)val).Dispose(); - IntPtr property = X11.XInternAtom(_display, "_NET_WM_ICON", onlyIfExists: false); - IntPtr type = X11.XInternAtom(_display, "CARDINAL", onlyIfExists: false); - fixed (uint* data = array) - { - X11.XChangeProperty(_display, _window, property, type, 32, 0, (nint)data, num5); - } - X11.XFlush(_display); - Console.WriteLine($"[X11Window] Set window icon: {width}x{height}"); - } - catch (Exception ex) - { - Console.WriteLine("[X11Window] Failed to set icon: " + ex.Message); - } - } + case XEventType.ButtonPress: + HandleButtonPress(ref xEvent.ButtonEvent); + break; - public void ProcessEvents() - { - int num = X11.XPending(_display); - if (num > 0) - { - if (_eventCounter % 100 == 0) - { - Console.WriteLine($"[X11Window] ProcessEvents: {num} pending events"); - } - _eventCounter++; - while (X11.XPending(_display) > 0) - { - Console.WriteLine("[X11Window] About to call XNextEvent"); - Console.Out.Flush(); - X11.XNextEvent(_display, out var eventReturn); - Console.WriteLine($"[X11Window] XNextEvent returned, type={eventReturn.Type}"); - Console.Out.Flush(); - HandleEvent(ref eventReturn); - } - } - } + case XEventType.ButtonRelease: + HandleButtonRelease(ref xEvent.ButtonEvent); + break; - public void Run() - { - _isRunning = true; - while (_isRunning) - { - X11.XNextEvent(_display, out var eventReturn); - HandleEvent(ref eventReturn); - } - } + case XEventType.MotionNotify: + HandleMotion(ref xEvent.MotionEvent); + break; - public void Stop() - { - _isRunning = false; - } + case XEventType.Expose: + if (xEvent.ExposeEvent.Count == 0) + { + Exposed?.Invoke(this, EventArgs.Empty); + } + break; - private void HandleEvent(ref XEvent xEvent) - { - Console.WriteLine($"[X11Window] Event: type={xEvent.Type}"); - switch (xEvent.Type) - { - case 2: - Console.WriteLine("[X11Window] KeyPress event"); - HandleKeyPress(ref xEvent.KeyEvent); - break; - case 3: - HandleKeyRelease(ref xEvent.KeyEvent); - break; - case 4: - Console.WriteLine($"[X11Window] ButtonPress event at ({xEvent.ButtonEvent.X}, {xEvent.ButtonEvent.Y}) button={xEvent.ButtonEvent.Button}"); - HandleButtonPress(ref xEvent.ButtonEvent); - break; - case 5: - HandleButtonRelease(ref xEvent.ButtonEvent); - break; - case 6: - HandleMotion(ref xEvent.MotionEvent); - break; - case 12: - if (xEvent.ExposeEvent.Count == 0) - { - this.Exposed?.Invoke(this, EventArgs.Empty); - } - break; - case 22: - HandleConfigure(ref xEvent.ConfigureEvent); - break; - case 9: - this.FocusGained?.Invoke(this, EventArgs.Empty); - break; - case 10: - this.FocusLost?.Invoke(this, EventArgs.Empty); - break; - case 33: - if (xEvent.ClientMessageEvent.Data.L0 == (long)_wmDeleteMessage) - { - this.CloseRequested?.Invoke(this, EventArgs.Empty); - _isRunning = false; - } - break; - } - } + case XEventType.ConfigureNotify: + HandleConfigure(ref xEvent.ConfigureEvent); + break; - private void HandleKeyPress(ref XKeyEvent keyEvent) - { - ulong keysym = KeyMapping.GetKeysym(_display, keyEvent.Keycode, (keyEvent.State & 1) != 0); - Key key = KeyMapping.FromKeysym(keysym); - KeyModifiers modifiers = KeyMapping.GetModifiers(keyEvent.State); - this.KeyDown?.Invoke(this, new KeyEventArgs(key, modifiers)); - bool flag = (keyEvent.State & 4) != 0; - bool flag2 = (keyEvent.State & 8) != 0; - if (keysym >= 32 && keysym <= 126 && !flag && !flag2) - { - this.TextInput?.Invoke(this, new TextInputEventArgs(((char)keysym).ToString())); - } - } + case XEventType.FocusIn: + FocusGained?.Invoke(this, EventArgs.Empty); + break; - private void HandleKeyRelease(ref XKeyEvent keyEvent) - { - Key key = KeyMapping.FromKeysym(KeyMapping.GetKeysym(_display, keyEvent.Keycode, (keyEvent.State & 1) != 0)); - KeyModifiers modifiers = KeyMapping.GetModifiers(keyEvent.State); - this.KeyUp?.Invoke(this, new KeyEventArgs(key, modifiers)); - } + case XEventType.FocusOut: + FocusLost?.Invoke(this, EventArgs.Empty); + break; - private void HandleButtonPress(ref XButtonEvent buttonEvent) - { - Console.WriteLine($"[X11Window] HandleButtonPress: button={buttonEvent.Button}, pos=({buttonEvent.X}, {buttonEvent.Y}), hasHandler={this.PointerPressed != null}"); - if (buttonEvent.Button == 4) - { - this.Scroll?.Invoke(this, new ScrollEventArgs(buttonEvent.X, buttonEvent.Y, 0f, -1f)); - return; - } - if (buttonEvent.Button == 5) - { - this.Scroll?.Invoke(this, new ScrollEventArgs(buttonEvent.X, buttonEvent.Y, 0f, 1f)); - return; - } - PointerButton pointerButton = MapButton(buttonEvent.Button); - Console.WriteLine($"[X11Window] Invoking PointerPressed with button={pointerButton}"); - this.PointerPressed?.Invoke(this, new PointerEventArgs(buttonEvent.X, buttonEvent.Y, pointerButton)); - } + case XEventType.ClientMessage: + if (xEvent.ClientMessageEvent.Data.L0 == (long)_wmDeleteMessage) + { + CloseRequested?.Invoke(this, EventArgs.Empty); + _isRunning = false; + } + break; + } + } - private void HandleButtonRelease(ref XButtonEvent buttonEvent) - { - Console.WriteLine($"[X11Window] HandleButtonRelease: button={buttonEvent.Button}, pos=({buttonEvent.X}, {buttonEvent.Y})"); - if (buttonEvent.Button != 4 && buttonEvent.Button != 5) - { - PointerButton button = MapButton(buttonEvent.Button); - Console.WriteLine($"[X11Window] Invoking PointerReleased, hasHandler={this.PointerReleased != null}"); - this.PointerReleased?.Invoke(this, new PointerEventArgs(buttonEvent.X, buttonEvent.Y, button)); - } - } + private void HandleKeyPress(ref XKeyEvent keyEvent) + { + var keysym = KeyMapping.GetKeysym(_display, keyEvent.Keycode, (keyEvent.State & 0x01) != 0); + var key = KeyMapping.FromKeysym(keysym); + var modifiers = KeyMapping.GetModifiers(keyEvent.State); - private void HandleMotion(ref XMotionEvent motionEvent) - { - this.PointerMoved?.Invoke(this, new PointerEventArgs(motionEvent.X, motionEvent.Y)); - } + KeyDown?.Invoke(this, new KeyEventArgs(key, modifiers)); - private void HandleConfigure(ref XConfigureEvent configureEvent) - { - if (configureEvent.Width != _width || configureEvent.Height != _height) - { - _width = configureEvent.Width; - _height = configureEvent.Height; - this.Resized?.Invoke(this, (_width, _height)); - } - } + // Generate text input for printable characters, but NOT when Control or Alt is held + // (those are keyboard shortcuts, not text input) + bool isControlHeld = (keyEvent.State & 0x04) != 0; // ControlMask + bool isAltHeld = (keyEvent.State & 0x08) != 0; // Mod1Mask (Alt) - private static PointerButton MapButton(uint button) - { - return button switch - { - 1u => PointerButton.Left, - 2u => PointerButton.Middle, - 3u => PointerButton.Right, - 8u => PointerButton.XButton1, - 9u => PointerButton.XButton2, - _ => PointerButton.None, - }; - } + if (keysym >= 32 && keysym <= 126 && !isControlHeld && !isAltHeld) + { + TextInput?.Invoke(this, new TextInputEventArgs(((char)keysym).ToString())); + } + } - public int GetFileDescriptor() - { - return X11.XConnectionNumber(_display); - } + private void HandleKeyRelease(ref XKeyEvent keyEvent) + { + var keysym = KeyMapping.GetKeysym(_display, keyEvent.Keycode, (keyEvent.State & 0x01) != 0); + var key = KeyMapping.FromKeysym(keysym); + var modifiers = KeyMapping.GetModifiers(keyEvent.State); - protected virtual void Dispose(bool disposing) - { - if (!_disposed) - { - if (_arrowCursor != IntPtr.Zero) - { - X11.XFreeCursor(_display, _arrowCursor); - _arrowCursor = IntPtr.Zero; - } - if (_handCursor != IntPtr.Zero) - { - X11.XFreeCursor(_display, _handCursor); - _handCursor = IntPtr.Zero; - } - if (_textCursor != IntPtr.Zero) - { - X11.XFreeCursor(_display, _textCursor); - _textCursor = IntPtr.Zero; - } - if (_window != IntPtr.Zero) - { - X11.XDestroyWindow(_display, _window); - _window = IntPtr.Zero; - } - if (_display != IntPtr.Zero) - { - X11.XCloseDisplay(_display); - _display = IntPtr.Zero; - } - _disposed = true; - } - } + KeyUp?.Invoke(this, new KeyEventArgs(key, modifiers)); + } - public unsafe void DrawPixels(IntPtr pixels, int width, int height, int stride) - { - if (_display == IntPtr.Zero || _window == IntPtr.Zero) - { - return; - } - IntPtr gc = X11.XDefaultGC(_display, _screen); - IntPtr visual = X11.XDefaultVisual(_display, _screen); - int depth = X11.XDefaultDepth(_display, _screen); - int num = height * stride; - IntPtr intPtr = Marshal.AllocHGlobal(num); - try - { - Buffer.MemoryCopy((void*)pixels, (void*)intPtr, num, num); - IntPtr intPtr2 = X11.XCreateImage(_display, visual, (uint)depth, 2, 0, intPtr, (uint)width, (uint)height, 32, stride); - if (intPtr2 != IntPtr.Zero) - { - X11.XPutImage(_display, _window, gc, intPtr2, 0, 0, 0, 0, (uint)width, (uint)height); - X11.XDestroyImage(intPtr2); - } - else - { - Marshal.FreeHGlobal(intPtr); - } - } - catch - { - Marshal.FreeHGlobal(intPtr); - throw; - } - X11.XFlush(_display); - } + private void HandleButtonPress(ref XButtonEvent buttonEvent) + { + // Buttons 4 and 5 are scroll wheel + if (buttonEvent.Button == 4) + { + Scroll?.Invoke(this, new ScrollEventArgs(buttonEvent.X, buttonEvent.Y, 0, -1)); + return; + } + if (buttonEvent.Button == 5) + { + Scroll?.Invoke(this, new ScrollEventArgs(buttonEvent.X, buttonEvent.Y, 0, 1)); + return; + } - public void Dispose() - { - Dispose(disposing: true); - GC.SuppressFinalize(this); - } + var button = MapButton(buttonEvent.Button); + PointerPressed?.Invoke(this, new PointerEventArgs(buttonEvent.X, buttonEvent.Y, button)); + } - ~X11Window() - { - Dispose(disposing: false); - } + private void HandleButtonRelease(ref XButtonEvent buttonEvent) + { + // Ignore scroll wheel releases + if (buttonEvent.Button == 4 || buttonEvent.Button == 5) + return; + + var button = MapButton(buttonEvent.Button); + PointerReleased?.Invoke(this, new PointerEventArgs(buttonEvent.X, buttonEvent.Y, button)); + } + + private void HandleMotion(ref XMotionEvent motionEvent) + { + PointerMoved?.Invoke(this, new PointerEventArgs(motionEvent.X, motionEvent.Y)); + } + + private void HandleConfigure(ref XConfigureEvent configureEvent) + { + if (configureEvent.Width != _width || configureEvent.Height != _height) + { + _width = configureEvent.Width; + _height = configureEvent.Height; + Resized?.Invoke(this, (_width, _height)); + } + } + + private static PointerButton MapButton(uint button) => button switch + { + 1 => PointerButton.Left, + 2 => PointerButton.Middle, + 3 => PointerButton.Right, + 8 => PointerButton.XButton1, + 9 => PointerButton.XButton2, + _ => PointerButton.None + }; + + /// + /// Gets the X11 file descriptor for use with select/poll. + /// + public int GetFileDescriptor() + { + return X11.XConnectionNumber(_display); + } + + #region IDisposable + + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + if (_window != IntPtr.Zero) + { + X11.XDestroyWindow(_display, _window); + _window = IntPtr.Zero; + } + + if (_display != IntPtr.Zero) + { + X11.XCloseDisplay(_display); + _display = IntPtr.Zero; + } + + _disposed = true; + } + } + + /// + /// Draws pixel data to the window. + /// + /// + /// Draws pixel data to the window. + /// + public void DrawPixels(IntPtr pixels, int width, int height, int stride) + { + if (_display == IntPtr.Zero || _window == IntPtr.Zero) return; + + var gc = X11.XDefaultGC(_display, _screen); + var visual = X11.XDefaultVisual(_display, _screen); + var depth = X11.XDefaultDepth(_display, _screen); + + // Allocate unmanaged memory and copy the pixel data + var dataSize = height * stride; + var unmanagedData = System.Runtime.InteropServices.Marshal.AllocHGlobal(dataSize); + + try + { + // Copy pixel data to unmanaged memory + unsafe + { + Buffer.MemoryCopy((void*)pixels, (void*)unmanagedData, dataSize, dataSize); + } + + // Create XImage from the unmanaged pixel data + var image = X11.XCreateImage( + _display, + visual, + (uint)depth, + X11.ZPixmap, + 0, + unmanagedData, + (uint)width, + (uint)height, + 32, + stride); + + if (image != IntPtr.Zero) + { + X11.XPutImage(_display, _window, gc, image, 0, 0, 0, 0, (uint)width, (uint)height); + X11.XDestroyImage(image); // This will free unmanagedData + } + else + { + // If XCreateImage failed, free the memory ourselves + System.Runtime.InteropServices.Marshal.FreeHGlobal(unmanagedData); + } + } + catch + { + System.Runtime.InteropServices.Marshal.FreeHGlobal(unmanagedData); + throw; + } + + X11.XFlush(_display); + } + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + ~X11Window() + { + Dispose(false); + } + + #endregion } diff --git a/fix_decompiler.py b/fix_decompiler.py new file mode 100644 index 0000000..0056f28 --- /dev/null +++ b/fix_decompiler.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +"""Fix decompiler artifacts in C# files.""" +import re +import sys +import os + +def fix_file(filepath): + with open(filepath, 'r') as f: + content = f.read() + + original = content + + # Pattern 1: Fix ((Type)(ref var))._002Ector(args) on same line as declaration + # Pattern: Type var = default(Type); followed by ((Type)(ref var))._002Ector(args); + # Combine: Type var = default(Type); + var._002Ector(args) -> Type var = new Type(args); + + # First, fix the _002Ector pattern to use "new Type(...)" + # Pattern: ((TypeName)(ref varName))._002Ector(args); + pattern_ctor = r'\(\((SK\w+|SKRect|SKSize|SKPoint|SKColor|Thickness|Font|LayoutOptions|SKFontMetrics|RectF|Rect)\)\(ref\s+(\w+)\)\)\._002Ector\(([^;]+)\);' + + def replace_ctor(match): + type_name = match.group(1) + var_name = match.group(2) + args = match.group(3) + return f'{var_name} = new {type_name}({args});' + + content = re.sub(pattern_ctor, replace_ctor, content) + + # Also handle simpler pattern: var._002Ector(args); + pattern_simple = r'(\w+)\._002Ector\(([^;]+)\);' + def replace_simple(match): + var_name = match.group(1) + args = match.group(2) + # We need to figure out the type from context - look for declaration + return f'// FIXME: {var_name} = new TYPE({args});' + + # Don't do the simple pattern - it's harder to fix without knowing the type + + # Pattern 2: Fix _003F (which is just ?) + content = content.replace('_003F', '?') + + # Pattern 2.5: Fix broken nullable cast patterns + # (((??)something) ?? fallback) -> (something ?? fallback) + content = re.sub(r'\(\(\(\?\?\)(\w+\.\w+)\)', r'(\1', content) + content = content.replace('((?)', '((') # Fix broken nullable casts + content = content.replace('(?))', '))') # Fix broken casts + + # Pattern 3: Clean up remaining ((Type)(ref var)) patterns without _002Ector + # These become just var + # First handle more types: Font, Thickness, Color, LayoutOptions, GridLength, etc. + types_to_fix = r'SK\w+|Font|Thickness|Color|LayoutOptions|SKFontMetrics|Rectangle|Point|Size|Rect|GridLength|GRGlFramebufferInfo|CornerRadius|RectF' + pattern_ref = r'\(\((' + types_to_fix + r')\)\(ref\s+(\w+)\)\)' + content = re.sub(pattern_ref, r'\2', content) + + # Pattern 3.5: Handle static property refs like ((SKColor)(ref SKColors.White)) + pattern_static_ref = r'\(\((' + types_to_fix + r')\)\(ref\s+(\w+\.\w+)\)\)' + content = re.sub(pattern_static_ref, r'\2', content) + + # Pattern 4: Also handle ViewHandler casts like ((ViewHandler)(object)handler) + # This should stay as-is but the inner (ref x) needs fixing first + + # Pattern 5: Fix simple (ref var) that might appear in other contexts + # Pattern: (ref varName) when standalone (not in a cast) + # Skip for now as this could break valid ref usage + + if content != original: + with open(filepath, 'w') as f: + f.write(content) + return True + return False + +def main(): + base_dir = '/Users/nible/Documents/GitHub/maui-linux-main' + count = 0 + for root, dirs, files in os.walk(base_dir): + # Skip hidden dirs and .git + dirs[:] = [d for d in dirs if not d.startswith('.')] + for fname in files: + if fname.endswith('.cs'): + filepath = os.path.join(root, fname) + if fix_file(filepath): + print(f'Fixed: {filepath}') + count += 1 + print(f'Fixed {count} files') + +if __name__ == '__main__': + main()