From 6f0d10935ca6a86db66e0c5dd807137115321c06 Mon Sep 17 00:00:00 2001 From: Dave Friedel Date: Thu, 1 Jan 2026 13:51:12 -0500 Subject: [PATCH] Fix handlers to match decompiled production code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ButtonHandler: Removed MapText/TextColor/Font (not in production), fixed namespace - LabelHandler: Added CharacterSpacing/LayoutAlignment/FormattedText, ConnectHandler gesture logic - EntryHandler: Added CharacterSpacing/ClearButtonVisibility/VerticalTextAlignment - EditorHandler: Created from decompiled (was missing) - SliderHandler: Fixed namespace, added ConnectHandler init calls - SwitchHandler: Added OffTrackColor logic, fixed namespace - CheckBoxHandler: Added VerticalLayoutAlignment/HorizontalLayoutAlignment - ProgressBarHandler: Added ConnectHandler/DisconnectHandler IsVisible tracking - PickerHandler: Created from decompiled with collection changed tracking - ActivityIndicatorHandler: Removed IsEnabled/BackgroundColor (not in production) - All handlers now use namespace Microsoft.Maui.Platform.Linux.Handlers - All handlers have proper null checks on PlatformView - Updated MERGE_TRACKING.md with accurate status 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- Handlers/ActivityIndicatorHandler.Linux.cs | 60 +- Handlers/ButtonHandler.Linux.cs | 148 ++--- Handlers/CheckBoxHandler.Linux.cs | 98 +-- Handlers/EditorHandler.Linux.cs | 187 ++++++ Handlers/EntryHandler.Linux.cs | 223 ++++--- Handlers/LabelHandler.Linux.cs | 339 +++++++--- Handlers/PickerHandler.Linux.cs | 178 +++++ Handlers/ProgressBarHandler.Linux.cs | 68 +- Handlers/SliderHandler.Linux.cs | 107 +-- Handlers/SwitchHandler.Linux.cs | 68 +- MERGE_TRACKING.md | 96 +-- Types/ToggledEventArgs.cs | 16 + Views/CheckedChangedEventArgs.cs | 16 + Views/SkiaActivityIndicator.cs | 10 +- Views/SkiaBorder.cs | 144 ++++- Views/SkiaBoxView.cs | 5 +- Views/SkiaButton.cs | 715 +++++++++------------ Views/SkiaCheckBox.cs | 173 ++--- Views/SkiaCollectionView.cs | 541 +++++++--------- Views/SkiaDatePicker.cs | 432 ++++++++----- Views/SkiaImage.cs | 334 ++++++++-- Views/SkiaLabel.cs | 12 +- Views/SkiaPicker.cs | 13 +- Views/SkiaProgressBar.cs | 29 +- Views/SkiaSlider.cs | 18 +- Views/SkiaSwitch.cs | 66 +- 26 files changed, 2502 insertions(+), 1594 deletions(-) create mode 100644 Handlers/EditorHandler.Linux.cs create mode 100644 Handlers/PickerHandler.Linux.cs create mode 100644 Types/ToggledEventArgs.cs create mode 100644 Views/CheckedChangedEventArgs.cs diff --git a/Handlers/ActivityIndicatorHandler.Linux.cs b/Handlers/ActivityIndicatorHandler.Linux.cs index 11b9722..3faa3b6 100644 --- a/Handlers/ActivityIndicatorHandler.Linux.cs +++ b/Handlers/ActivityIndicatorHandler.Linux.cs @@ -1,63 +1,63 @@ // 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.Graphics; using Microsoft.Maui.Handlers; -namespace Microsoft.Maui.Platform; +namespace Microsoft.Maui.Platform.Linux.Handlers; /// /// Linux handler for ActivityIndicator control. /// -public partial class ActivityIndicatorHandler : ViewHandler +public class ActivityIndicatorHandler : ViewHandler { public static IPropertyMapper Mapper = new PropertyMapper(ViewHandler.ViewMapper) { - [nameof(IActivityIndicator.IsRunning)] = MapIsRunning, - [nameof(IActivityIndicator.Color)] = MapColor, - [nameof(IView.IsEnabled)] = MapIsEnabled, - [nameof(IView.Background)] = MapBackground, - ["BackgroundColor"] = MapBackgroundColor, + ["IsRunning"] = MapIsRunning, + ["Color"] = MapColor, + ["Background"] = MapBackground }; public static CommandMapper CommandMapper = new(ViewHandler.ViewCommandMapper); - public ActivityIndicatorHandler() : base(Mapper, CommandMapper) { } + public ActivityIndicatorHandler() : base(Mapper, CommandMapper) + { + } - protected override SkiaActivityIndicator CreatePlatformView() => new SkiaActivityIndicator(); + public ActivityIndicatorHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null) + : base(mapper ?? Mapper, commandMapper ?? CommandMapper) + { + } + + protected override SkiaActivityIndicator CreatePlatformView() + { + return new SkiaActivityIndicator(); + } public static void MapIsRunning(ActivityIndicatorHandler handler, IActivityIndicator activityIndicator) { - handler.PlatformView.IsRunning = activityIndicator.IsRunning; + if (handler.PlatformView != null) + { + handler.PlatformView.IsRunning = activityIndicator.IsRunning; + } } public static void MapColor(ActivityIndicatorHandler handler, IActivityIndicator activityIndicator) { - if (activityIndicator.Color != null) + if (handler.PlatformView != null && activityIndicator.Color != null) + { handler.PlatformView.Color = activityIndicator.Color.ToSKColor(); - handler.PlatformView.Invalidate(); - } - - public static void MapIsEnabled(ActivityIndicatorHandler handler, IActivityIndicator activityIndicator) - { - handler.PlatformView.IsEnabled = activityIndicator.IsEnabled; - handler.PlatformView.Invalidate(); + } } public static void MapBackground(ActivityIndicatorHandler handler, IActivityIndicator activityIndicator) { - if (activityIndicator.Background is SolidColorBrush solidBrush && solidBrush.Color != null) + if (handler.PlatformView != null) { - handler.PlatformView.BackgroundColor = solidBrush.Color.ToSKColor(); - handler.PlatformView.Invalidate(); - } - } - - public static void MapBackgroundColor(ActivityIndicatorHandler handler, IActivityIndicator activityIndicator) - { - if (activityIndicator is Microsoft.Maui.Controls.VisualElement ve && ve.BackgroundColor != null) - { - handler.PlatformView.BackgroundColor = ve.BackgroundColor.ToSKColor(); - handler.PlatformView.Invalidate(); + if (activityIndicator.Background is SolidPaint solidPaint && solidPaint.Color != null) + { + handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor(); + } } } } diff --git a/Handlers/ButtonHandler.Linux.cs b/Handlers/ButtonHandler.Linux.cs index 54feb11..3b850d0 100644 --- a/Handlers/ButtonHandler.Linux.cs +++ b/Handlers/ButtonHandler.Linux.cs @@ -1,57 +1,42 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; +using Microsoft.Maui.Graphics; using Microsoft.Maui.Handlers; using SkiaSharp; -namespace Microsoft.Maui.Platform; +namespace Microsoft.Maui.Platform.Linux.Handlers; /// /// Linux handler for Button control. /// -public partial class ButtonHandler : ViewHandler +public class ButtonHandler : ViewHandler { - /// - /// Maps the property mapper for the handler. - /// public static IPropertyMapper Mapper = new PropertyMapper(ViewHandler.ViewMapper) { - [nameof(IButton.Text)] = MapText, - [nameof(IButton.TextColor)] = MapTextColor, - [nameof(IButton.Background)] = MapBackground, - [nameof(IButton.Font)] = MapFont, - [nameof(IButton.Padding)] = MapPadding, - [nameof(IButton.CornerRadius)] = MapCornerRadius, - [nameof(IButton.BorderColor)] = MapBorderColor, - [nameof(IButton.BorderWidth)] = MapBorderWidth, - [nameof(IView.IsEnabled)] = MapIsEnabled, + ["StrokeColor"] = MapStrokeColor, + ["StrokeThickness"] = MapStrokeThickness, + ["CornerRadius"] = MapCornerRadius, + ["Background"] = MapBackground, + ["Padding"] = MapPadding, + ["IsEnabled"] = MapIsEnabled }; - /// - /// Maps the command mapper for the handler. - /// - public static CommandMapper CommandMapper = new(ViewHandler.ViewCommandMapper) - { - }; + public static CommandMapper CommandMapper = new(ViewHandler.ViewCommandMapper); public ButtonHandler() : base(Mapper, CommandMapper) { } - public ButtonHandler(IPropertyMapper? mapper) - : base(mapper ?? Mapper, CommandMapper) - { - } - - public ButtonHandler(IPropertyMapper? mapper, CommandMapper? commandMapper) + public ButtonHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null) : base(mapper ?? Mapper, commandMapper ?? CommandMapper) { } protected override SkiaButton CreatePlatformView() { - var button = new SkiaButton(); - return button; + return new SkiaButton(); } protected override void ConnectHandler(SkiaButton platformView) @@ -61,18 +46,13 @@ public partial class ButtonHandler : ViewHandler platformView.Pressed += OnPressed; platformView.Released += OnReleased; - // Manually map all properties on connect since MAUI may not trigger updates - // for properties that were set before handler connection if (VirtualView != null) { - MapText(this, VirtualView); - MapTextColor(this, VirtualView); - MapBackground(this, VirtualView); - MapFont(this, VirtualView); - MapPadding(this, VirtualView); + MapStrokeColor(this, VirtualView); + MapStrokeThickness(this, VirtualView); MapCornerRadius(this, VirtualView); - MapBorderColor(this, VirtualView); - MapBorderWidth(this, VirtualView); + MapBackground(this, VirtualView); + MapPadding(this, VirtualView); MapIsEnabled(this, VirtualView); } } @@ -100,80 +80,66 @@ public partial class ButtonHandler : ViewHandler VirtualView?.Released(); } - public static void MapText(ButtonHandler handler, IButton button) + public static void MapStrokeColor(ButtonHandler handler, IButton button) { - handler.PlatformView.Text = button.Text ?? ""; - handler.PlatformView.Invalidate(); - } - - public static void MapTextColor(ButtonHandler handler, IButton button) - { - if (button.TextColor != null) + if (handler.PlatformView != null) { - handler.PlatformView.TextColor = button.TextColor.ToSKColor(); + var strokeColor = button.StrokeColor; + if (strokeColor != null) + { + handler.PlatformView.BorderColor = strokeColor.ToSKColor(); + } } - handler.PlatformView.Invalidate(); } - public static void MapBackground(ButtonHandler handler, IButton button) + public static void MapStrokeThickness(ButtonHandler handler, IButton button) { - var background = button.Background; - if (background is SolidColorBrush solidBrush && solidBrush.Color != null) + if (handler.PlatformView != null) { - // Use ButtonBackgroundColor which is used for rendering, not base BackgroundColor - handler.PlatformView.ButtonBackgroundColor = solidBrush.Color.ToSKColor(); + handler.PlatformView.BorderWidth = (float)button.StrokeThickness; } - handler.PlatformView.Invalidate(); - } - - public static void MapFont(ButtonHandler handler, IButton button) - { - var font = button.Font; - if (font.Family != null) - { - handler.PlatformView.FontFamily = font.Family; - } - handler.PlatformView.FontSize = (float)font.Size; - handler.PlatformView.IsBold = font.Weight == FontWeight.Bold; - handler.PlatformView.Invalidate(); - } - - public static void MapPadding(ButtonHandler handler, IButton button) - { - var padding = button.Padding; - handler.PlatformView.Padding = new SKRect( - (float)padding.Left, - (float)padding.Top, - (float)padding.Right, - (float)padding.Bottom); - handler.PlatformView.Invalidate(); } public static void MapCornerRadius(ButtonHandler handler, IButton button) { - handler.PlatformView.CornerRadius = button.CornerRadius; - handler.PlatformView.Invalidate(); - } - - public static void MapBorderColor(ButtonHandler handler, IButton button) - { - if (button.StrokeColor != null) + if (handler.PlatformView != null) { - handler.PlatformView.BorderColor = button.StrokeColor.ToSKColor(); + handler.PlatformView.CornerRadius = button.CornerRadius; } - handler.PlatformView.Invalidate(); } - public static void MapBorderWidth(ButtonHandler handler, IButton button) + public static void MapBackground(ButtonHandler handler, IButton button) { - handler.PlatformView.BorderWidth = (float)button.StrokeThickness; - handler.PlatformView.Invalidate(); + if (handler.PlatformView != null) + { + var background = button.Background; + if (background is SolidPaint solidPaint && solidPaint.Color != null) + { + handler.PlatformView.ButtonBackgroundColor = solidPaint.Color.ToSKColor(); + } + } + } + + public static void MapPadding(ButtonHandler handler, IButton button) + { + if (handler.PlatformView != null) + { + 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) { - Console.WriteLine($"[ButtonHandler] MapIsEnabled called - Text='{handler.PlatformView.Text}', IsEnabled={button.IsEnabled}"); - handler.PlatformView.IsEnabled = button.IsEnabled; - handler.PlatformView.Invalidate(); + if (handler.PlatformView != null) + { + Console.WriteLine($"[ButtonHandler] MapIsEnabled - Text='{handler.PlatformView.Text}', IsEnabled={button.IsEnabled}"); + handler.PlatformView.IsEnabled = button.IsEnabled; + handler.PlatformView.Invalidate(); + } } } diff --git a/Handlers/CheckBoxHandler.Linux.cs b/Handlers/CheckBoxHandler.Linux.cs index d8d81e8..706a38f 100644 --- a/Handlers/CheckBoxHandler.Linux.cs +++ b/Handlers/CheckBoxHandler.Linux.cs @@ -1,45 +1,35 @@ // 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.Handlers; -using SkiaSharp; +using Microsoft.Maui.Primitives; -namespace Microsoft.Maui.Platform; +namespace Microsoft.Maui.Platform.Linux.Handlers; /// /// Linux handler for CheckBox control. /// -public partial class CheckBoxHandler : ViewHandler +public class CheckBoxHandler : ViewHandler { - /// - /// Maps the property mapper for the handler. - /// public static IPropertyMapper Mapper = new PropertyMapper(ViewHandler.ViewMapper) { - [nameof(ICheckBox.IsChecked)] = MapIsChecked, - [nameof(ICheckBox.Foreground)] = MapForeground, - [nameof(IView.IsEnabled)] = MapIsEnabled, - [nameof(IView.Background)] = MapBackground, - ["BackgroundColor"] = MapBackgroundColor, + ["IsChecked"] = MapIsChecked, + ["Foreground"] = MapForeground, + ["Background"] = MapBackground, + ["IsEnabled"] = MapIsEnabled, + ["VerticalLayoutAlignment"] = MapVerticalLayoutAlignment, + ["HorizontalLayoutAlignment"] = MapHorizontalLayoutAlignment }; - /// - /// Maps the command mapper for the handler. - /// - public static CommandMapper CommandMapper = new(ViewHandler.ViewCommandMapper) - { - }; + public static CommandMapper CommandMapper = new(ViewHandler.ViewCommandMapper); public CheckBoxHandler() : base(Mapper, CommandMapper) { } - public CheckBoxHandler(IPropertyMapper? mapper) - : base(mapper ?? Mapper, CommandMapper) - { - } - - public CheckBoxHandler(IPropertyMapper? mapper, CommandMapper? commandMapper) + public CheckBoxHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null) : base(mapper ?? Mapper, commandMapper ?? CommandMapper) { } @@ -71,7 +61,7 @@ public partial class CheckBoxHandler : ViewHandler public static void MapIsChecked(CheckBoxHandler handler, ICheckBox checkBox) { - if (handler.PlatformView.IsChecked != checkBox.IsChecked) + if (handler.PlatformView != null) { handler.PlatformView.IsChecked = checkBox.IsChecked; } @@ -79,35 +69,61 @@ public partial class CheckBoxHandler : ViewHandler public static void MapForeground(CheckBoxHandler handler, ICheckBox checkBox) { - var foreground = checkBox.Foreground; - if (foreground is SolidColorBrush solidBrush && solidBrush.Color != null) + if (handler.PlatformView != null) { - handler.PlatformView.BoxColor = solidBrush.Color.ToSKColor(); + if (checkBox.Foreground is SolidPaint solidPaint && solidPaint.Color != null) + { + handler.PlatformView.CheckColor = solidPaint.Color.ToSKColor(); + } } - handler.PlatformView.Invalidate(); - } - - public static void MapIsEnabled(CheckBoxHandler handler, ICheckBox checkBox) - { - handler.PlatformView.IsEnabled = checkBox.IsEnabled; - handler.PlatformView.Invalidate(); } public static void MapBackground(CheckBoxHandler handler, ICheckBox checkBox) { - if (checkBox.Background is SolidColorBrush solidBrush && solidBrush.Color != null) + if (handler.PlatformView != null) { - handler.PlatformView.BackgroundColor = solidBrush.Color.ToSKColor(); - handler.PlatformView.Invalidate(); + if (checkBox.Background is SolidPaint solidPaint && solidPaint.Color != null) + { + handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor(); + } } } - public static void MapBackgroundColor(CheckBoxHandler handler, ICheckBox checkBox) + public static void MapIsEnabled(CheckBoxHandler handler, ICheckBox checkBox) { - if (checkBox is Microsoft.Maui.Controls.VisualElement ve && ve.BackgroundColor != null) + if (handler.PlatformView != null) { - handler.PlatformView.BackgroundColor = ve.BackgroundColor.ToSKColor(); - handler.PlatformView.Invalidate(); + handler.PlatformView.IsEnabled = checkBox.IsEnabled; + } + } + + public static void MapVerticalLayoutAlignment(CheckBoxHandler handler, ICheckBox checkBox) + { + if (handler.PlatformView != null) + { + handler.PlatformView.VerticalOptions = (int)checkBox.VerticalLayoutAlignment switch + { + 1 => LayoutOptions.Start, + 2 => LayoutOptions.Center, + 3 => LayoutOptions.End, + 0 => LayoutOptions.Fill, + _ => LayoutOptions.Fill + }; + } + } + + public static void MapHorizontalLayoutAlignment(CheckBoxHandler handler, ICheckBox checkBox) + { + if (handler.PlatformView != null) + { + handler.PlatformView.HorizontalOptions = (int)checkBox.HorizontalLayoutAlignment switch + { + 1 => LayoutOptions.Start, + 2 => LayoutOptions.Center, + 3 => LayoutOptions.End, + 0 => LayoutOptions.Fill, + _ => LayoutOptions.Start + }; } } } diff --git a/Handlers/EditorHandler.Linux.cs b/Handlers/EditorHandler.Linux.cs new file mode 100644 index 0000000..a40c417 --- /dev/null +++ b/Handlers/EditorHandler.Linux.cs @@ -0,0 +1,187 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Maui.Controls; +using Microsoft.Maui.Graphics; +using Microsoft.Maui.Handlers; + +namespace Microsoft.Maui.Platform.Linux.Handlers; + +/// +/// Linux handler for Editor control. +/// +public class EditorHandler : ViewHandler +{ + public static IPropertyMapper Mapper = new PropertyMapper(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 CommandMapper CommandMapper = new(ViewHandler.ViewCommandMapper); + + public EditorHandler() : base(Mapper, CommandMapper) + { + } + + public EditorHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null) + : base(mapper ?? Mapper, commandMapper ?? CommandMapper) + { + } + + 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 DisconnectHandler(SkiaEditor platformView) + { + platformView.TextChanged -= OnTextChanged; + platformView.Completed -= OnCompleted; + base.DisconnectHandler(platformView); + } + + private void OnTextChanged(object? sender, EventArgs e) + { + if (VirtualView != null && PlatformView != null) + { + VirtualView.Text = PlatformView.Text; + } + } + + private void OnCompleted(object? sender, EventArgs e) + { + // Editor completed - no specific action needed + } + + public static void MapText(EditorHandler handler, IEditor editor) + { + if (handler.PlatformView != null) + { + handler.PlatformView.Text = editor.Text ?? ""; + handler.PlatformView.Invalidate(); + } + } + + public static void MapPlaceholder(EditorHandler handler, IEditor editor) + { + if (handler.PlatformView != null) + { + handler.PlatformView.Placeholder = editor.Placeholder ?? ""; + } + } + + public static void MapPlaceholderColor(EditorHandler handler, IEditor editor) + { + if (handler.PlatformView != null && editor.PlaceholderColor != null) + { + handler.PlatformView.PlaceholderColor = editor.PlaceholderColor.ToSKColor(); + } + } + + public static void MapTextColor(EditorHandler handler, IEditor editor) + { + if (handler.PlatformView != null && editor.TextColor != null) + { + handler.PlatformView.TextColor = editor.TextColor.ToSKColor(); + } + } + + public static void MapCharacterSpacing(EditorHandler handler, IEditor editor) + { + // Character spacing not implemented for editor + } + + public static void MapIsReadOnly(EditorHandler handler, IEditor editor) + { + if (handler.PlatformView != null) + { + handler.PlatformView.IsReadOnly = editor.IsReadOnly; + } + } + + public static void MapIsTextPredictionEnabled(EditorHandler handler, IEditor editor) + { + // Text prediction is a mobile feature + } + + public static void MapMaxLength(EditorHandler handler, IEditor editor) + { + if (handler.PlatformView != null) + { + handler.PlatformView.MaxLength = editor.MaxLength; + } + } + + public static void MapCursorPosition(EditorHandler handler, IEditor editor) + { + if (handler.PlatformView != null) + { + handler.PlatformView.CursorPosition = editor.CursorPosition; + } + } + + public static void MapSelectionLength(EditorHandler handler, IEditor editor) + { + // Selection length not implemented + } + + public static void MapKeyboard(EditorHandler handler, IEditor editor) + { + // Keyboard type is a mobile feature + } + + public static void MapHorizontalTextAlignment(EditorHandler handler, IEditor editor) + { + // Horizontal text alignment not implemented + } + + public static void MapVerticalTextAlignment(EditorHandler handler, IEditor editor) + { + // Vertical text alignment not implemented + } + + public static void MapBackground(EditorHandler handler, IEditor editor) + { + if (handler.PlatformView != null) + { + if (editor.Background is SolidPaint solidPaint && solidPaint.Color != null) + { + handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor(); + } + } + } + + public static void MapBackgroundColor(EditorHandler handler, IEditor editor) + { + if (handler.PlatformView != null) + { + if (editor is VisualElement ve && ve.BackgroundColor != null) + { + handler.PlatformView.BackgroundColor = ve.BackgroundColor.ToSKColor(); + handler.PlatformView.Invalidate(); + } + } + } +} diff --git a/Handlers/EntryHandler.Linux.cs b/Handlers/EntryHandler.Linux.cs index 1f2eac6..db07b6c 100644 --- a/Handlers/EntryHandler.Linux.cs +++ b/Handlers/EntryHandler.Linux.cs @@ -1,55 +1,47 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; +using Microsoft.Maui.Controls; +using Microsoft.Maui.Graphics; using Microsoft.Maui.Handlers; using SkiaSharp; -namespace Microsoft.Maui.Platform; +namespace Microsoft.Maui.Platform.Linux.Handlers; /// /// Linux handler for Entry control. /// -public partial class EntryHandler : ViewHandler +public class EntryHandler : ViewHandler { - /// - /// Maps the property mapper for the handler. - /// public static IPropertyMapper Mapper = new PropertyMapper(ViewHandler.ViewMapper) { - [nameof(IEntry.Text)] = MapText, - [nameof(IEntry.TextColor)] = MapTextColor, - [nameof(IEntry.Placeholder)] = MapPlaceholder, - [nameof(IEntry.PlaceholderColor)] = MapPlaceholderColor, - [nameof(IEntry.Font)] = MapFont, - [nameof(IEntry.IsPassword)] = MapIsPassword, - [nameof(IEntry.MaxLength)] = MapMaxLength, - [nameof(IEntry.IsReadOnly)] = MapIsReadOnly, - [nameof(IEntry.HorizontalTextAlignment)] = MapHorizontalTextAlignment, - [nameof(IEntry.CursorPosition)] = MapCursorPosition, - [nameof(IEntry.SelectionLength)] = MapSelectionLength, - [nameof(IEntry.ReturnType)] = MapReturnType, - [nameof(IView.IsEnabled)] = MapIsEnabled, - [nameof(IEntry.Background)] = MapBackground, - ["BackgroundColor"] = MapBackgroundColor, + ["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 }; - /// - /// Maps the command mapper for the handler. - /// - public static CommandMapper CommandMapper = new(ViewHandler.ViewCommandMapper) - { - }; + public static CommandMapper CommandMapper = new(ViewHandler.ViewCommandMapper); public EntryHandler() : base(Mapper, CommandMapper) { } - public EntryHandler(IPropertyMapper? mapper) - : base(mapper ?? Mapper, CommandMapper) - { - } - - public EntryHandler(IPropertyMapper? mapper, CommandMapper? commandMapper) + public EntryHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null) : base(mapper ?? Mapper, commandMapper ?? CommandMapper) { } @@ -75,9 +67,9 @@ public partial class EntryHandler : ViewHandler private void OnTextChanged(object? sender, TextChangedEventArgs e) { - if (VirtualView != null && VirtualView.Text != e.NewText) + if (VirtualView != null && PlatformView != null && VirtualView.Text != e.NewTextValue) { - VirtualView.Text = e.NewText; + VirtualView.Text = e.NewTextValue ?? string.Empty; } } @@ -88,112 +80,173 @@ public partial class EntryHandler : ViewHandler public static void MapText(EntryHandler handler, IEntry entry) { - if (handler.PlatformView.Text != entry.Text) + if (handler.PlatformView != null && handler.PlatformView.Text != entry.Text) { - handler.PlatformView.Text = entry.Text ?? ""; + handler.PlatformView.Text = entry.Text ?? string.Empty; + handler.PlatformView.Invalidate(); } } public static void MapTextColor(EntryHandler handler, IEntry entry) { - if (entry.TextColor != null) + if (handler.PlatformView != null && entry.TextColor != null) { handler.PlatformView.TextColor = entry.TextColor.ToSKColor(); } - handler.PlatformView.Invalidate(); - } - - public static void MapPlaceholder(EntryHandler handler, IEntry entry) - { - handler.PlatformView.Placeholder = entry.Placeholder ?? ""; - handler.PlatformView.Invalidate(); - } - - public static void MapPlaceholderColor(EntryHandler handler, IEntry entry) - { - if (entry.PlaceholderColor != null) - { - handler.PlatformView.PlaceholderColor = entry.PlaceholderColor.ToSKColor(); - } - handler.PlatformView.Invalidate(); } public static void MapFont(EntryHandler handler, IEntry entry) { - var font = entry.Font; - if (font.Family != null) + if (handler.PlatformView != null) { - handler.PlatformView.FontFamily = font.Family; + var font = entry.Font; + if (font.Size > 0) + { + handler.PlatformView.FontSize = (float)font.Size; + } + if (!string.IsNullOrEmpty(font.Family)) + { + handler.PlatformView.FontFamily = font.Family; + } + handler.PlatformView.IsBold = (int)font.Weight >= 700; + handler.PlatformView.IsItalic = font.Slant == FontSlant.Italic || font.Slant == FontSlant.Oblique; } - handler.PlatformView.FontSize = (float)font.Size; - handler.PlatformView.Invalidate(); } - public static void MapIsPassword(EntryHandler handler, IEntry entry) + public static void MapCharacterSpacing(EntryHandler handler, IEntry entry) { - handler.PlatformView.IsPassword = entry.IsPassword; - handler.PlatformView.Invalidate(); + if (handler.PlatformView != null) + { + handler.PlatformView.CharacterSpacing = (float)entry.CharacterSpacing; + } } - public static void MapMaxLength(EntryHandler handler, IEntry entry) + public static void MapPlaceholder(EntryHandler handler, IEntry entry) { - handler.PlatformView.MaxLength = entry.MaxLength; + if (handler.PlatformView != null) + { + handler.PlatformView.Placeholder = entry.Placeholder ?? string.Empty; + } + } + + public static void MapPlaceholderColor(EntryHandler handler, IEntry entry) + { + if (handler.PlatformView != null && entry.PlaceholderColor != null) + { + handler.PlatformView.PlaceholderColor = entry.PlaceholderColor.ToSKColor(); + } } public static void MapIsReadOnly(EntryHandler handler, IEntry entry) { - handler.PlatformView.IsReadOnly = entry.IsReadOnly; + if (handler.PlatformView != null) + { + handler.PlatformView.IsReadOnly = entry.IsReadOnly; + } } - public static void MapHorizontalTextAlignment(EntryHandler handler, IEntry entry) + public static void MapMaxLength(EntryHandler handler, IEntry entry) { - handler.PlatformView.HorizontalTextAlignment = entry.HorizontalTextAlignment switch + if (handler.PlatformView != null) { - Microsoft.Maui.TextAlignment.Start => TextAlignment.Start, - Microsoft.Maui.TextAlignment.Center => TextAlignment.Center, - Microsoft.Maui.TextAlignment.End => TextAlignment.End, - _ => TextAlignment.Start - }; - handler.PlatformView.Invalidate(); + handler.PlatformView.MaxLength = entry.MaxLength; + } } public static void MapCursorPosition(EntryHandler handler, IEntry entry) { - handler.PlatformView.CursorPosition = entry.CursorPosition; + if (handler.PlatformView != null) + { + handler.PlatformView.CursorPosition = entry.CursorPosition; + } } public static void MapSelectionLength(EntryHandler handler, IEntry entry) { - // Selection length is handled internally by SkiaEntry + if (handler.PlatformView != null) + { + handler.PlatformView.SelectionLength = entry.SelectionLength; + } + } + + public static void MapIsPassword(EntryHandler handler, IEntry entry) + { + if (handler.PlatformView != null) + { + handler.PlatformView.IsPassword = entry.IsPassword; + } } public static void MapReturnType(EntryHandler handler, IEntry entry) { - // Return type affects keyboard on mobile; on desktop, Enter always completes + // ReturnType affects keyboard on mobile; access PlatformView to ensure it exists + _ = handler.PlatformView; } - public static void MapIsEnabled(EntryHandler handler, IEntry entry) + public static void MapClearButtonVisibility(EntryHandler handler, IEntry entry) { - handler.PlatformView.IsEnabled = entry.IsEnabled; - handler.PlatformView.Invalidate(); + if (handler.PlatformView != null) + { + // ClearButtonVisibility.WhileEditing = 1 + handler.PlatformView.ShowClearButton = (int)entry.ClearButtonVisibility == 1; + } + } + + public static void MapHorizontalTextAlignment(EntryHandler handler, IEntry entry) + { + if (handler.PlatformView != null) + { + handler.PlatformView.HorizontalTextAlignment = (int)entry.HorizontalTextAlignment switch + { + 0 => TextAlignment.Start, + 1 => TextAlignment.Center, + 2 => TextAlignment.End, + _ => TextAlignment.Start + }; + } + } + + public static void MapVerticalTextAlignment(EntryHandler handler, IEntry entry) + { + if (handler.PlatformView != null) + { + handler.PlatformView.VerticalTextAlignment = (int)entry.VerticalTextAlignment switch + { + 0 => TextAlignment.Start, + 1 => TextAlignment.Center, + 2 => TextAlignment.End, + _ => TextAlignment.Center + }; + } } public static void MapBackground(EntryHandler handler, IEntry entry) { - var background = entry.Background; - if (background is SolidColorBrush solidBrush && solidBrush.Color != null) + if (handler.PlatformView != null) { - handler.PlatformView.BackgroundColor = solidBrush.Color.ToSKColor(); + if (entry.Background is SolidPaint solidPaint && solidPaint.Color != null) + { + handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor(); + } } - handler.PlatformView.Invalidate(); } public static void MapBackgroundColor(EntryHandler handler, IEntry entry) { - if (entry is Microsoft.Maui.Controls.VisualElement ve && ve.BackgroundColor != null) + if (handler.PlatformView == null) { - handler.PlatformView.BackgroundColor = ve.BackgroundColor.ToSKColor(); - handler.PlatformView.Invalidate(); + return; + } + + if (entry is Entry mauiEntry) + { + Console.WriteLine($"[EntryHandler] MapBackgroundColor: {mauiEntry.BackgroundColor}"); + if (mauiEntry.BackgroundColor != null) + { + var color = mauiEntry.BackgroundColor.ToSKColor(); + Console.WriteLine($"[EntryHandler] Setting EntryBackgroundColor to: {color}"); + handler.PlatformView.EntryBackgroundColor = color; + } } } } diff --git a/Handlers/LabelHandler.Linux.cs b/Handlers/LabelHandler.Linux.cs index 3df3fd4..a9562c0 100644 --- a/Handlers/LabelHandler.Linux.cs +++ b/Handlers/LabelHandler.Linux.cs @@ -1,52 +1,49 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Maui.Controls; +using Microsoft.Maui.Graphics; using Microsoft.Maui.Handlers; +using Microsoft.Maui.Platform.Linux.Window; +using Microsoft.Maui.Primitives; using SkiaSharp; -namespace Microsoft.Maui.Platform; +namespace Microsoft.Maui.Platform.Linux.Handlers; /// /// Linux handler for Label control. /// -public partial class LabelHandler : ViewHandler +public class LabelHandler : ViewHandler { - /// - /// Maps the property mapper for the handler. - /// public static IPropertyMapper Mapper = new PropertyMapper(ViewHandler.ViewMapper) { - [nameof(ILabel.Text)] = MapText, - [nameof(ILabel.TextColor)] = MapTextColor, - [nameof(ILabel.Font)] = MapFont, - [nameof(ILabel.HorizontalTextAlignment)] = MapHorizontalTextAlignment, - [nameof(ILabel.VerticalTextAlignment)] = MapVerticalTextAlignment, - [nameof(ILabel.LineBreakMode)] = MapLineBreakMode, - [nameof(ILabel.MaxLines)] = MapMaxLines, - [nameof(ILabel.Padding)] = MapPadding, - [nameof(ILabel.TextDecorations)] = MapTextDecorations, - [nameof(ILabel.LineHeight)] = MapLineHeight, - [nameof(ILabel.Background)] = MapBackground, - ["BackgroundColor"] = MapBackgroundColor, + ["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 }; - /// - /// Maps the command mapper for the handler. - /// - public static CommandMapper CommandMapper = new(ViewHandler.ViewCommandMapper) - { - }; + public static CommandMapper CommandMapper = new(ViewHandler.ViewCommandMapper); public LabelHandler() : base(Mapper, CommandMapper) { } - public LabelHandler(IPropertyMapper? mapper) - : base(mapper ?? Mapper, CommandMapper) - { - } - - public LabelHandler(IPropertyMapper? mapper, CommandMapper? commandMapper) + public LabelHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null) : base(mapper ?? Mapper, commandMapper ?? CommandMapper) { } @@ -56,119 +53,263 @@ public partial class LabelHandler : ViewHandler return new SkiaLabel(); } + protected override void ConnectHandler(SkiaLabel platformView) + { + base.ConnectHandler(platformView); + + if (VirtualView is View view) + { + platformView.MauiView = view; + if (view.GestureRecognizers.OfType().Any()) + { + platformView.CursorType = CursorType.Hand; + } + } + + platformView.Tapped += OnPlatformViewTapped; + } + + protected override void DisconnectHandler(SkiaLabel platformView) + { + platformView.Tapped -= OnPlatformViewTapped; + platformView.MauiView = null; + base.DisconnectHandler(platformView); + } + + private void OnPlatformViewTapped(object? sender, EventArgs e) + { + if (VirtualView is View view) + { + GestureManager.ProcessTap(view, 0.0, 0.0); + } + } + public static void MapText(LabelHandler handler, ILabel label) { - handler.PlatformView.Text = label.Text ?? ""; - handler.PlatformView.Invalidate(); + if (handler.PlatformView != null) + { + handler.PlatformView.Text = label.Text ?? string.Empty; + } } public static void MapTextColor(LabelHandler handler, ILabel label) { - if (label.TextColor != null) + if (handler.PlatformView != null && label.TextColor != null) { handler.PlatformView.TextColor = label.TextColor.ToSKColor(); } - handler.PlatformView.Invalidate(); } public static void MapFont(LabelHandler handler, ILabel label) { - var font = label.Font; - if (font.Family != null) + if (handler.PlatformView != null) { - handler.PlatformView.FontFamily = font.Family; + var font = label.Font; + if (font.Size > 0) + { + handler.PlatformView.FontSize = (float)font.Size; + } + if (!string.IsNullOrEmpty(font.Family)) + { + handler.PlatformView.FontFamily = font.Family; + } + handler.PlatformView.IsBold = (int)font.Weight >= 700; + handler.PlatformView.IsItalic = font.Slant == FontSlant.Italic || font.Slant == FontSlant.Oblique; + } + } + + public static void MapCharacterSpacing(LabelHandler handler, ILabel label) + { + if (handler.PlatformView != null) + { + handler.PlatformView.CharacterSpacing = (float)label.CharacterSpacing; } - handler.PlatformView.FontSize = (float)font.Size; - handler.PlatformView.IsBold = font.Weight == FontWeight.Bold; - handler.PlatformView.IsItalic = font.Slant == FontSlant.Italic; - handler.PlatformView.Invalidate(); } public static void MapHorizontalTextAlignment(LabelHandler handler, ILabel label) { - handler.PlatformView.HorizontalTextAlignment = label.HorizontalTextAlignment switch + if (handler.PlatformView != null) { - Microsoft.Maui.TextAlignment.Start => TextAlignment.Start, - Microsoft.Maui.TextAlignment.Center => TextAlignment.Center, - Microsoft.Maui.TextAlignment.End => TextAlignment.End, - _ => TextAlignment.Start - }; - handler.PlatformView.Invalidate(); + handler.PlatformView.HorizontalTextAlignment = (int)label.HorizontalTextAlignment switch + { + 0 => TextAlignment.Start, + 1 => TextAlignment.Center, + 2 => TextAlignment.End, + _ => TextAlignment.Start + }; + } } public static void MapVerticalTextAlignment(LabelHandler handler, ILabel label) { - handler.PlatformView.VerticalTextAlignment = label.VerticalTextAlignment switch + if (handler.PlatformView != null) { - Microsoft.Maui.TextAlignment.Start => TextAlignment.Start, - Microsoft.Maui.TextAlignment.Center => TextAlignment.Center, - Microsoft.Maui.TextAlignment.End => TextAlignment.End, - _ => TextAlignment.Center - }; - handler.PlatformView.Invalidate(); - } - - public static void MapLineBreakMode(LabelHandler handler, ILabel label) - { - handler.PlatformView.LineBreakMode = label.LineBreakMode switch - { - Microsoft.Maui.LineBreakMode.NoWrap => LineBreakMode.NoWrap, - Microsoft.Maui.LineBreakMode.WordWrap => LineBreakMode.WordWrap, - Microsoft.Maui.LineBreakMode.CharacterWrap => LineBreakMode.CharacterWrap, - Microsoft.Maui.LineBreakMode.HeadTruncation => LineBreakMode.HeadTruncation, - Microsoft.Maui.LineBreakMode.TailTruncation => LineBreakMode.TailTruncation, - Microsoft.Maui.LineBreakMode.MiddleTruncation => LineBreakMode.MiddleTruncation, - _ => LineBreakMode.TailTruncation - }; - handler.PlatformView.Invalidate(); - } - - public static void MapMaxLines(LabelHandler handler, ILabel label) - { - handler.PlatformView.MaxLines = label.MaxLines; - handler.PlatformView.Invalidate(); - } - - public static void MapPadding(LabelHandler handler, ILabel label) - { - var padding = label.Padding; - handler.PlatformView.Padding = new SKRect( - (float)padding.Left, - (float)padding.Top, - (float)padding.Right, - (float)padding.Bottom); - handler.PlatformView.Invalidate(); + handler.PlatformView.VerticalTextAlignment = (int)label.VerticalTextAlignment switch + { + 0 => TextAlignment.Start, + 1 => TextAlignment.Center, + 2 => TextAlignment.End, + _ => TextAlignment.Center + }; + } } public static void MapTextDecorations(LabelHandler handler, ILabel label) { - var decorations = label.TextDecorations; - handler.PlatformView.IsUnderline = decorations.HasFlag(TextDecorations.Underline); - handler.PlatformView.IsStrikethrough = decorations.HasFlag(TextDecorations.Strikethrough); - handler.PlatformView.Invalidate(); + if (handler.PlatformView != null) + { + handler.PlatformView.IsUnderline = (label.TextDecorations & TextDecorations.Underline) != 0; + handler.PlatformView.IsStrikethrough = (label.TextDecorations & TextDecorations.Strikethrough) != 0; + } } public static void MapLineHeight(LabelHandler handler, ILabel label) { - handler.PlatformView.LineHeight = (float)label.LineHeight; - handler.PlatformView.Invalidate(); + if (handler.PlatformView != null) + { + handler.PlatformView.LineHeight = (float)label.LineHeight; + } + } + + public static void MapLineBreakMode(LabelHandler handler, ILabel label) + { + if (handler.PlatformView != null) + { + if (label is Label mauiLabel) + { + handler.PlatformView.LineBreakMode = (int)mauiLabel.LineBreakMode switch + { + 0 => LineBreakMode.NoWrap, + 1 => LineBreakMode.WordWrap, + 2 => LineBreakMode.CharacterWrap, + 3 => LineBreakMode.HeadTruncation, + 4 => LineBreakMode.TailTruncation, + 5 => LineBreakMode.MiddleTruncation, + _ => LineBreakMode.TailTruncation + }; + } + } + } + + public static void MapMaxLines(LabelHandler handler, ILabel label) + { + if (handler.PlatformView != null) + { + if (label is Label mauiLabel) + { + handler.PlatformView.MaxLines = mauiLabel.MaxLines; + } + } + } + + public static void MapPadding(LabelHandler handler, ILabel label) + { + if (handler.PlatformView != null) + { + 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 (label.Background is SolidColorBrush solidBrush && solidBrush.Color != null) + if (handler.PlatformView != null) { - handler.PlatformView.BackgroundColor = solidBrush.Color.ToSKColor(); - handler.PlatformView.Invalidate(); + if (label.Background is SolidPaint solidPaint && solidPaint.Color != null) + { + handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor(); + } } } - public static void MapBackgroundColor(LabelHandler handler, ILabel label) + public static void MapVerticalLayoutAlignment(LabelHandler handler, ILabel label) { - if (label is Microsoft.Maui.Controls.VisualElement ve && ve.BackgroundColor != null) + if (handler.PlatformView != null) { - handler.PlatformView.BackgroundColor = ve.BackgroundColor.ToSKColor(); - handler.PlatformView.Invalidate(); + handler.PlatformView.VerticalOptions = (int)label.VerticalLayoutAlignment switch + { + 1 => LayoutOptions.Start, + 2 => LayoutOptions.Center, + 3 => LayoutOptions.End, + 0 => LayoutOptions.Fill, + _ => LayoutOptions.Start + }; } } + + public static void MapHorizontalLayoutAlignment(LabelHandler handler, ILabel label) + { + if (handler.PlatformView != null) + { + handler.PlatformView.HorizontalOptions = (int)label.HorizontalLayoutAlignment switch + { + 1 => LayoutOptions.Start, + 2 => LayoutOptions.Center, + 3 => LayoutOptions.End, + 0 => LayoutOptions.Fill, + _ => LayoutOptions.Start + }; + } + } + + public static void MapFormattedText(LabelHandler handler, ILabel label) + { + if (handler.PlatformView == null) + { + return; + } + + if (label is not Label mauiLabel) + { + handler.PlatformView.FormattedSpans = null; + return; + } + + var formattedText = mauiLabel.FormattedText; + if (formattedText == null || formattedText.Spans.Count == 0) + { + handler.PlatformView.FormattedSpans = null; + return; + } + + var spans = new List(); + foreach (var span in formattedText.Spans) + { + var skiaSpan = new SkiaTextSpan + { + Text = span.Text ?? "", + IsBold = span.FontAttributes.HasFlag(FontAttributes.Bold), + IsItalic = span.FontAttributes.HasFlag(FontAttributes.Italic), + IsUnderline = (span.TextDecorations & TextDecorations.Underline) != 0, + IsStrikethrough = (span.TextDecorations & TextDecorations.Strikethrough) != 0, + CharacterSpacing = (float)span.CharacterSpacing, + LineHeight = (float)span.LineHeight + }; + + if (span.TextColor != null) + { + skiaSpan.TextColor = span.TextColor.ToSKColor(); + } + if (span.BackgroundColor != null) + { + skiaSpan.BackgroundColor = span.BackgroundColor.ToSKColor(); + } + if (!string.IsNullOrEmpty(span.FontFamily)) + { + skiaSpan.FontFamily = span.FontFamily; + } + if (span.FontSize > 0) + { + skiaSpan.FontSize = (float)span.FontSize; + } + + spans.Add(skiaSpan); + } + + handler.PlatformView.FormattedSpans = spans; + } } diff --git a/Handlers/PickerHandler.Linux.cs b/Handlers/PickerHandler.Linux.cs new file mode 100644 index 0000000..62a4a52 --- /dev/null +++ b/Handlers/PickerHandler.Linux.cs @@ -0,0 +1,178 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Specialized; +using System.Linq; +using Microsoft.Maui.Controls; +using Microsoft.Maui.Graphics; +using Microsoft.Maui.Handlers; + +namespace Microsoft.Maui.Platform.Linux.Handlers; + +/// +/// Linux handler for Picker control. +/// +public class PickerHandler : ViewHandler +{ + public static IPropertyMapper Mapper = new PropertyMapper(ViewHandler.ViewMapper) + { + ["Title"] = MapTitle, + ["TitleColor"] = MapTitleColor, + ["SelectedIndex"] = MapSelectedIndex, + ["TextColor"] = MapTextColor, + ["Font"] = MapFont, + ["CharacterSpacing"] = MapCharacterSpacing, + ["HorizontalTextAlignment"] = MapHorizontalTextAlignment, + ["VerticalTextAlignment"] = MapVerticalTextAlignment, + ["Background"] = MapBackground, + ["ItemsSource"] = MapItemsSource + }; + + public static CommandMapper CommandMapper = new(ViewHandler.ViewCommandMapper); + + private INotifyCollectionChanged? _itemsCollection; + + public PickerHandler() : base(Mapper, CommandMapper) + { + } + + public PickerHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null) + : base(mapper ?? Mapper, commandMapper ?? CommandMapper) + { + } + + protected override SkiaPicker CreatePlatformView() + { + return new SkiaPicker(); + } + + protected override void ConnectHandler(SkiaPicker platformView) + { + base.ConnectHandler(platformView); + platformView.SelectedIndexChanged += OnSelectedIndexChanged; + + if (VirtualView is Picker picker && picker.Items is INotifyCollectionChanged itemsCollection) + { + _itemsCollection = itemsCollection; + _itemsCollection.CollectionChanged += OnItemsCollectionChanged; + } + + ReloadItems(); + } + + protected override void DisconnectHandler(SkiaPicker platformView) + { + platformView.SelectedIndexChanged -= OnSelectedIndexChanged; + + if (_itemsCollection != null) + { + _itemsCollection.CollectionChanged -= OnItemsCollectionChanged; + _itemsCollection = null; + } + + base.DisconnectHandler(platformView); + } + + private void OnItemsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + ReloadItems(); + } + + private void OnSelectedIndexChanged(object? sender, EventArgs e) + { + if (VirtualView != null && PlatformView != null) + { + VirtualView.SelectedIndex = PlatformView.SelectedIndex; + } + } + + private void ReloadItems() + { + if (PlatformView != null && VirtualView != null) + { + var items = VirtualView.GetItemsAsArray(); + PlatformView.SetItems(items.Select(i => i?.ToString() ?? "")); + } + } + + public static void MapTitle(PickerHandler handler, IPicker picker) + { + if (handler.PlatformView != null) + { + handler.PlatformView.Title = picker.Title ?? ""; + } + } + + public static void MapTitleColor(PickerHandler handler, IPicker picker) + { + if (handler.PlatformView != null && picker.TitleColor != null) + { + handler.PlatformView.TitleColor = picker.TitleColor.ToSKColor(); + } + } + + public static void MapSelectedIndex(PickerHandler handler, IPicker picker) + { + if (handler.PlatformView != null) + { + handler.PlatformView.SelectedIndex = picker.SelectedIndex; + } + } + + public static void MapTextColor(PickerHandler handler, IPicker picker) + { + if (handler.PlatformView != null && picker.TextColor != null) + { + handler.PlatformView.TextColor = picker.TextColor.ToSKColor(); + } + } + + public static void MapFont(PickerHandler handler, IPicker picker) + { + if (handler.PlatformView != null) + { + var font = picker.Font; + if (!string.IsNullOrEmpty(font.Family)) + { + handler.PlatformView.FontFamily = font.Family; + } + if (font.Size > 0) + { + handler.PlatformView.FontSize = (float)font.Size; + } + handler.PlatformView.Invalidate(); + } + } + + public static void MapCharacterSpacing(PickerHandler handler, IPicker picker) + { + // Character spacing not implemented + } + + public static void MapHorizontalTextAlignment(PickerHandler handler, IPicker picker) + { + // Horizontal text alignment not implemented + } + + public static void MapVerticalTextAlignment(PickerHandler handler, IPicker picker) + { + // Vertical text alignment not implemented + } + + public static void MapBackground(PickerHandler handler, IPicker picker) + { + if (handler.PlatformView != null) + { + if (picker.Background is SolidPaint solidPaint && solidPaint.Color != null) + { + handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor(); + } + } + } + + public static void MapItemsSource(PickerHandler handler, IPicker picker) + { + handler.ReloadItems(); + } +} diff --git a/Handlers/ProgressBarHandler.Linux.cs b/Handlers/ProgressBarHandler.Linux.cs index e12f5fe..284c45a 100644 --- a/Handlers/ProgressBarHandler.Linux.cs +++ b/Handlers/ProgressBarHandler.Linux.cs @@ -1,29 +1,71 @@ // 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.Maui.Controls; +using Microsoft.Maui.Graphics; using Microsoft.Maui.Handlers; -namespace Microsoft.Maui.Platform; +namespace Microsoft.Maui.Platform.Linux.Handlers; /// /// Linux handler for ProgressBar control. /// -public partial class ProgressBarHandler : ViewHandler +public class ProgressBarHandler : ViewHandler { public static IPropertyMapper Mapper = new PropertyMapper(ViewHandler.ViewMapper) { - [nameof(IProgress.Progress)] = MapProgress, - [nameof(IProgress.ProgressColor)] = MapProgressColor, - [nameof(IView.IsEnabled)] = MapIsEnabled, - [nameof(IView.Background)] = MapBackground, - ["BackgroundColor"] = MapBackgroundColor, + ["Progress"] = MapProgress, + ["ProgressColor"] = MapProgressColor, + ["IsEnabled"] = MapIsEnabled, + ["Background"] = MapBackground, + ["BackgroundColor"] = MapBackgroundColor }; public static CommandMapper CommandMapper = new(ViewHandler.ViewCommandMapper); - public ProgressBarHandler() : base(Mapper, CommandMapper) { } + public ProgressBarHandler() : base(Mapper, CommandMapper) + { + } - protected override SkiaProgressBar CreatePlatformView() => new SkiaProgressBar(); + protected override SkiaProgressBar CreatePlatformView() + { + return new SkiaProgressBar(); + } + + protected override void ConnectHandler(SkiaProgressBar platformView) + { + base.ConnectHandler(platformView); + + if (VirtualView is BindableObject bindable) + { + bindable.PropertyChanged += OnVirtualViewPropertyChanged; + } + + if (VirtualView is VisualElement ve) + { + platformView.IsVisible = ve.IsVisible; + } + } + + protected override void DisconnectHandler(SkiaProgressBar platformView) + { + if (VirtualView is BindableObject bindable) + { + bindable.PropertyChanged -= OnVirtualViewPropertyChanged; + } + + base.DisconnectHandler(platformView); + } + + private void OnVirtualViewPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (VirtualView is VisualElement ve && e.PropertyName == "IsVisible") + { + PlatformView.IsVisible = ve.IsVisible; + PlatformView.Invalidate(); + } + } public static void MapProgress(ProgressBarHandler handler, IProgress progress) { @@ -33,7 +75,9 @@ public partial class ProgressBarHandler : ViewHandler /// Linux handler for Slider control. /// -public partial class SliderHandler : ViewHandler +public class SliderHandler : ViewHandler { public static IPropertyMapper Mapper = new PropertyMapper(ViewHandler.ViewMapper) { - [nameof(ISlider.Minimum)] = MapMinimum, - [nameof(ISlider.Maximum)] = MapMaximum, - [nameof(ISlider.Value)] = MapValue, - [nameof(ISlider.MinimumTrackColor)] = MapMinimumTrackColor, - [nameof(ISlider.MaximumTrackColor)] = MapMaximumTrackColor, - [nameof(ISlider.ThumbColor)] = MapThumbColor, - [nameof(IView.IsEnabled)] = MapIsEnabled, - [nameof(IView.Background)] = MapBackground, - ["BackgroundColor"] = MapBackgroundColor, + ["Minimum"] = MapMinimum, + ["Maximum"] = MapMaximum, + ["Value"] = MapValue, + ["MinimumTrackColor"] = MapMinimumTrackColor, + ["MaximumTrackColor"] = MapMaximumTrackColor, + ["ThumbColor"] = MapThumbColor, + ["Background"] = MapBackground, + ["IsEnabled"] = MapIsEnabled }; public static CommandMapper CommandMapper = new(ViewHandler.ViewCommandMapper); - public SliderHandler() : base(Mapper, CommandMapper) { } + public SliderHandler() : base(Mapper, CommandMapper) + { + } - protected override SkiaSlider CreatePlatformView() => new SkiaSlider(); + public SliderHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null) + : base(mapper ?? Mapper, commandMapper ?? CommandMapper) + { + } + + protected override SkiaSlider CreatePlatformView() + { + return new SkiaSlider(); + } protected override void ConnectHandler(SkiaSlider platformView) { @@ -35,6 +46,14 @@ public partial class SliderHandler : ViewHandler platformView.ValueChanged += OnValueChanged; platformView.DragStarted += OnDragStarted; platformView.DragCompleted += OnDragCompleted; + + if (VirtualView != null) + { + MapMinimum(this, VirtualView); + MapMaximum(this, VirtualView); + MapValue(this, VirtualView); + MapIsEnabled(this, VirtualView); + } } protected override void DisconnectHandler(SkiaSlider platformView) @@ -47,30 +66,41 @@ public partial class SliderHandler : ViewHandler private void OnValueChanged(object? sender, SliderValueChangedEventArgs e) { - if (VirtualView != null && Math.Abs(VirtualView.Value - e.NewValue) > 0.001) + if (VirtualView != null && PlatformView != null && Math.Abs(VirtualView.Value - e.NewValue) > 0.0001) { VirtualView.Value = e.NewValue; } } - private void OnDragStarted(object? sender, EventArgs e) => VirtualView?.DragStarted(); - private void OnDragCompleted(object? sender, EventArgs e) => VirtualView?.DragCompleted(); + private void OnDragStarted(object? sender, EventArgs e) + { + VirtualView?.DragStarted(); + } + + private void OnDragCompleted(object? sender, EventArgs e) + { + VirtualView?.DragCompleted(); + } public static void MapMinimum(SliderHandler handler, ISlider slider) { - handler.PlatformView.Minimum = slider.Minimum; - handler.PlatformView.Invalidate(); + if (handler.PlatformView != null) + { + handler.PlatformView.Minimum = slider.Minimum; + } } public static void MapMaximum(SliderHandler handler, ISlider slider) { - handler.PlatformView.Maximum = slider.Maximum; - handler.PlatformView.Invalidate(); + if (handler.PlatformView != null) + { + handler.PlatformView.Maximum = slider.Maximum; + } } public static void MapValue(SliderHandler handler, ISlider slider) { - if (Math.Abs(handler.PlatformView.Value - slider.Value) > 0.001) + if (handler.PlatformView != null && Math.Abs(handler.PlatformView.Value - slider.Value) > 0.0001) { handler.PlatformView.Value = slider.Value; } @@ -78,45 +108,44 @@ public partial class SliderHandler : ViewHandler public static void MapMinimumTrackColor(SliderHandler handler, ISlider slider) { - if (slider.MinimumTrackColor != null) + if (handler.PlatformView != null && slider.MinimumTrackColor != null) + { handler.PlatformView.ActiveTrackColor = slider.MinimumTrackColor.ToSKColor(); - handler.PlatformView.Invalidate(); + } } public static void MapMaximumTrackColor(SliderHandler handler, ISlider slider) { - if (slider.MaximumTrackColor != null) + if (handler.PlatformView != null && slider.MaximumTrackColor != null) + { handler.PlatformView.TrackColor = slider.MaximumTrackColor.ToSKColor(); - handler.PlatformView.Invalidate(); + } } public static void MapThumbColor(SliderHandler handler, ISlider slider) { - if (slider.ThumbColor != null) + if (handler.PlatformView != null && slider.ThumbColor != null) + { handler.PlatformView.ThumbColor = slider.ThumbColor.ToSKColor(); - handler.PlatformView.Invalidate(); - } - - public static void MapIsEnabled(SliderHandler handler, ISlider slider) - { - handler.PlatformView.IsEnabled = slider.IsEnabled; - handler.PlatformView.Invalidate(); + } } public static void MapBackground(SliderHandler handler, ISlider slider) { - if (slider.Background is SolidColorBrush solidBrush && solidBrush.Color != null) + if (handler.PlatformView != null) { - handler.PlatformView.BackgroundColor = solidBrush.Color.ToSKColor(); - handler.PlatformView.Invalidate(); + if (slider.Background is SolidPaint solidPaint && solidPaint.Color != null) + { + handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor(); + } } } - public static void MapBackgroundColor(SliderHandler handler, ISlider slider) + public static void MapIsEnabled(SliderHandler handler, ISlider slider) { - if (slider is Microsoft.Maui.Controls.VisualElement ve && ve.BackgroundColor != null) + if (handler.PlatformView != null) { - handler.PlatformView.BackgroundColor = ve.BackgroundColor.ToSKColor(); + handler.PlatformView.IsEnabled = slider.IsEnabled; handler.PlatformView.Invalidate(); } } diff --git a/Handlers/SwitchHandler.Linux.cs b/Handlers/SwitchHandler.Linux.cs index fd9873e..79d9ae1 100644 --- a/Handlers/SwitchHandler.Linux.cs +++ b/Handlers/SwitchHandler.Linux.cs @@ -1,30 +1,41 @@ // 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.Graphics; using Microsoft.Maui.Handlers; +using SkiaSharp; -namespace Microsoft.Maui.Platform; +namespace Microsoft.Maui.Platform.Linux.Handlers; /// /// Linux handler for Switch control. /// -public partial class SwitchHandler : ViewHandler +public class SwitchHandler : ViewHandler { public static IPropertyMapper Mapper = new PropertyMapper(ViewHandler.ViewMapper) { - [nameof(ISwitch.IsOn)] = MapIsOn, - [nameof(ISwitch.TrackColor)] = MapTrackColor, - [nameof(ISwitch.ThumbColor)] = MapThumbColor, - [nameof(IView.IsEnabled)] = MapIsEnabled, - [nameof(IView.Background)] = MapBackground, - ["BackgroundColor"] = MapBackgroundColor, + ["IsOn"] = MapIsOn, + ["TrackColor"] = MapTrackColor, + ["ThumbColor"] = MapThumbColor, + ["Background"] = MapBackground, + ["IsEnabled"] = MapIsEnabled }; public static CommandMapper CommandMapper = new(ViewHandler.ViewCommandMapper); - public SwitchHandler() : base(Mapper, CommandMapper) { } + public SwitchHandler() : base(Mapper, CommandMapper) + { + } - protected override SkiaSwitch CreatePlatformView() => new SkiaSwitch(); + public SwitchHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null) + : base(mapper ?? Mapper, commandMapper ?? CommandMapper) + { + } + + protected override SkiaSwitch CreatePlatformView() + { + return new SkiaSwitch(); + } protected override void ConnectHandler(SkiaSwitch platformView) { @@ -48,7 +59,7 @@ public partial class SwitchHandler : ViewHandler public static void MapIsOn(SwitchHandler handler, ISwitch @switch) { - if (handler.PlatformView.IsOn != @switch.IsOn) + if (handler.PlatformView != null) { handler.PlatformView.IsOn = @switch.IsOn; } @@ -56,39 +67,38 @@ public partial class SwitchHandler : ViewHandler public static void MapTrackColor(SwitchHandler handler, ISwitch @switch) { - if (@switch.TrackColor != null) - handler.PlatformView.OnTrackColor = @switch.TrackColor.ToSKColor(); - handler.PlatformView.Invalidate(); + if (handler.PlatformView != null && @switch.TrackColor != null) + { + var onTrackColor = @switch.TrackColor.ToSKColor(); + handler.PlatformView.OnTrackColor = onTrackColor; + handler.PlatformView.OffTrackColor = onTrackColor.WithAlpha(128); + } } public static void MapThumbColor(SwitchHandler handler, ISwitch @switch) { - if (@switch.ThumbColor != null) + if (handler.PlatformView != null && @switch.ThumbColor != null) + { handler.PlatformView.ThumbColor = @switch.ThumbColor.ToSKColor(); - handler.PlatformView.Invalidate(); - } - - public static void MapIsEnabled(SwitchHandler handler, ISwitch @switch) - { - handler.PlatformView.IsEnabled = @switch.IsEnabled; - handler.PlatformView.Invalidate(); + } } public static void MapBackground(SwitchHandler handler, ISwitch @switch) { - if (@switch.Background is SolidColorBrush solidBrush && solidBrush.Color != null) + if (handler.PlatformView != null) { - handler.PlatformView.BackgroundColor = solidBrush.Color.ToSKColor(); - handler.PlatformView.Invalidate(); + if (@switch.Background is SolidPaint solidPaint && solidPaint.Color != null) + { + handler.PlatformView.BackgroundColor = solidPaint.Color.ToSKColor(); + } } } - public static void MapBackgroundColor(SwitchHandler handler, ISwitch @switch) + public static void MapIsEnabled(SwitchHandler handler, ISwitch @switch) { - if (@switch is Microsoft.Maui.Controls.VisualElement ve && ve.BackgroundColor != null) + if (handler.PlatformView != null) { - handler.PlatformView.BackgroundColor = ve.BackgroundColor.ToSKColor(); - handler.PlatformView.Invalidate(); + handler.PlatformView.IsEnabled = @switch.IsEnabled; } } } diff --git a/MERGE_TRACKING.md b/MERGE_TRACKING.md index fac3a69..ac642d7 100644 --- a/MERGE_TRACKING.md +++ b/MERGE_TRACKING.md @@ -8,47 +8,49 @@ ## HANDLERS +**CRITICAL**: All handlers must use namespace `Microsoft.Maui.Platform.Linux.Handlers` and follow decompiled EXACTLY. + | File | Status | Notes | |------|--------|-------| -| ActivityIndicatorHandler.cs | [x] | Verified - matches decompiled | -| ApplicationHandler.cs | [x] | Verified - matches decompiled | +| ActivityIndicatorHandler.Linux.cs | [x] | **FIXED 2026-01-01** - Removed IsEnabled/BackgroundColor (not in production), fixed namespace | +| ApplicationHandler.cs | [ ] | NEEDS VERIFICATION | | BorderHandler.cs | [ ] | BLOCKED - needs SkiaBorder.MauiView and Tapped | -| BoxViewHandler.cs | [x] | Verified | -| ButtonHandler.cs | [x] | Contains TextButtonHandler - Verified | -| CheckBoxHandler.cs | [x] | Verified | -| CollectionViewHandler.cs | [x] | FIXED - Added OnItemTapped gesture handling, MauiView assignment | -| DatePickerHandler.cs | [x] | Verified | -| EditorHandler.cs | [x] | Verified | -| EntryHandler.cs | [x] | Verified | -| FlexLayoutHandler.cs | [x] | Verified - matches decompiled | -| FlyoutPageHandler.cs | [x] | Verified - matches decompiled | +| BoxViewHandler.cs | [ ] | NEEDS VERIFICATION | +| ButtonHandler.Linux.cs | [x] | **FIXED 2026-01-01** - Removed MapText/TextColor/Font (not in production), fixed namespace, added null checks | +| CheckBoxHandler.Linux.cs | [x] | **FIXED 2026-01-01** - Added VerticalLayoutAlignment/HorizontalLayoutAlignment, fixed namespace | +| CollectionViewHandler.cs | [ ] | NEEDS VERIFICATION | +| DatePickerHandler.cs | [ ] | NEEDS VERIFICATION | +| EditorHandler.Linux.cs | [x] | **CREATED 2026-01-01** - Was missing, created from decompiled | +| EntryHandler.Linux.cs | [x] | **FIXED 2026-01-01** - Added CharacterSpacing/ClearButtonVisibility/VerticalTextAlignment, fixed namespace, null checks | +| FlexLayoutHandler.cs | [ ] | NEEDS VERIFICATION | +| FlyoutPageHandler.cs | [ ] | NEEDS VERIFICATION | | FrameHandler.cs | [ ] | BLOCKED - needs SkiaFrame.MauiView and Tapped event | -| GestureManager.cs | [x] | FIXED - Added third fallback (TappedEvent fields), type info dump, swipe Right handling | -| GraphicsViewHandler.cs | [x] | Verified - matches decompiled | +| GestureManager.cs | [ ] | NEEDS VERIFICATION | +| GraphicsViewHandler.cs | [ ] | NEEDS VERIFICATION | | GtkWebViewHandler.cs | [x] | Added new file from decompiled | -| GtkWebViewManager.cs | [ ] | | -| GtkWebViewPlatformView.cs | [ ] | | +| GtkWebViewManager.cs | [ ] | NEEDS VERIFICATION | +| GtkWebViewPlatformView.cs | [ ] | NEEDS VERIFICATION | | GtkWebViewProxy.cs | [x] | Added new file from decompiled | -| ImageButtonHandler.cs | [x] | FIXED - added MapBackgroundColor | -| ImageHandler.cs | [x] | Verified | -| ItemsViewHandler.cs | [x] | Verified - matches decompiled | -| LabelHandler.cs | [x] | Verified | -| LayoutHandler.cs | [x] | Contains GridHandler, StackLayoutHandler, LayoutHandlerUpdate - Verified | -| NavigationPageHandler.cs | [x] | FIXED - Added LoadToolbarIcon, Icon loading, content handling, animated params | -| PageHandler.cs | [x] | Added MapBackgroundColor | -| PickerHandler.cs | [x] | Verified | -| ProgressBarHandler.cs | [x] | Verified | -| RadioButtonHandler.cs | [x] | Verified - matches decompiled | -| ScrollViewHandler.cs | [x] | Verified | -| SearchBarHandler.cs | [x] | Verified - matches decompiled | -| ShellHandler.cs | [x] | Verified - matches decompiled | -| SliderHandler.cs | [x] | Verified | -| StepperHandler.cs | [x] | FIXED - Added MapIncrement, MapIsEnabled, dark theme colors | -| SwitchHandler.cs | [x] | Verified | -| TabbedPageHandler.cs | [x] | Verified - matches decompiled | -| TimePickerHandler.cs | [x] | FIXED - Added dark theme colors | -| WebViewHandler.cs | [x] | Fixed namespace-qualified event args | -| WindowHandler.cs | [x] | Verified - Contains SkiaWindow, SizeChangedEventArgs | +| ImageButtonHandler.cs | [ ] | NEEDS VERIFICATION | +| ImageHandler.cs | [ ] | NEEDS VERIFICATION | +| ItemsViewHandler.cs | [ ] | NEEDS VERIFICATION | +| LabelHandler.Linux.cs | [x] | **FIXED 2026-01-01** - Added CharacterSpacing/LayoutAlignment/FormattedText, ConnectHandler gesture logic, fixed namespace | +| LayoutHandler.cs | [ ] | NEEDS VERIFICATION | +| NavigationPageHandler.cs | [ ] | NEEDS VERIFICATION | +| PageHandler.cs | [ ] | NEEDS VERIFICATION | +| PickerHandler.Linux.cs | [x] | **CREATED 2026-01-01** - Was missing, created from decompiled with collection changed tracking | +| ProgressBarHandler.Linux.cs | [x] | **FIXED 2026-01-01** - Added ConnectHandler/DisconnectHandler IsVisible tracking, fixed namespace | +| RadioButtonHandler.cs | [ ] | NEEDS VERIFICATION | +| ScrollViewHandler.cs | [ ] | NEEDS VERIFICATION | +| SearchBarHandler.cs | [ ] | NEEDS VERIFICATION | +| ShellHandler.cs | [ ] | NEEDS VERIFICATION | +| SliderHandler.Linux.cs | [x] | **FIXED 2026-01-01** - Removed BackgroundColor (use base), fixed namespace, added ConnectHandler init calls | +| StepperHandler.cs | [ ] | NEEDS VERIFICATION | +| SwitchHandler.Linux.cs | [x] | **FIXED 2026-01-01** - Added OffTrackColor logic, fixed namespace, removed extra BackgroundColor | +| TabbedPageHandler.cs | [ ] | NEEDS VERIFICATION | +| TimePickerHandler.cs | [ ] | NEEDS VERIFICATION | +| WebViewHandler.cs | [ ] | NEEDS VERIFICATION | +| WindowHandler.cs | [ ] | NEEDS VERIFICATION | --- @@ -56,13 +58,13 @@ | File | Status | Notes | |------|--------|-------| -| SkiaActivityIndicator.cs | [ ] | | +| SkiaActivityIndicator.cs | [x] | Verified - all TwoWay, logic matches | | SkiaAlertDialog.cs | [ ] | | | SkiaBorder.cs | [ ] | Contains SkiaFrame | -| SkiaBoxView.cs | [ ] | | -| SkiaButton.cs | [ ] | | +| SkiaBoxView.cs | [x] | Verified - all TwoWay, logic matches | +| SkiaButton.cs | [x] | Verified - all TwoWay, logic matches | | SkiaCarouselView.cs | [ ] | | -| SkiaCheckBox.cs | [ ] | | +| SkiaCheckBox.cs | [x] | Verified - IsChecked=OneWay, rest TwoWay, logic matches | | SkiaCollectionView.cs | [ ] | | | SkiaContentPresenter.cs | [ ] | | | SkiaContextMenu.cs | [ ] | | @@ -81,17 +83,17 @@ | SkiaMenuBar.cs | [ ] | Contains MenuItem, MenuBarItem | | SkiaNavigationPage.cs | [ ] | | | SkiaPage.cs | [x] | Added SkiaToolbarItem.Icon property | -| SkiaPicker.cs | [ ] | | -| SkiaProgressBar.cs | [ ] | | +| SkiaPicker.cs | [x] | FIXED - SelectedIndex=OneWay, all others=TwoWay (was missing) | +| SkiaProgressBar.cs | [x] | Verified - Progress=OneWay, rest TwoWay, logic matches | | SkiaRadioButton.cs | [ ] | | | SkiaRefreshView.cs | [ ] | | | SkiaScrollView.cs | [ ] | | | SkiaSearchBar.cs | [ ] | | | SkiaShell.cs | [ ] | Contains ShellSection, ShellContent | -| SkiaSlider.cs | [ ] | | +| SkiaSlider.cs | [x] | FIXED - Value=OneWay, rest TwoWay (agent had inverted all) | | SkiaStepper.cs | [ ] | | | SkiaSwipeView.cs | [ ] | | -| SkiaSwitch.cs | [ ] | | +| SkiaSwitch.cs | [x] | FIXED - IsOn=OneWay (agent had TwoWay) | | SkiaTabbedPage.cs | [ ] | | | SkiaTemplatedView.cs | [ ] | | | SkiaTimePicker.cs | [ ] | | @@ -204,3 +206,11 @@ |------|--------|-------| | LinuxApplication.cs | [ ] | | | LinuxApplicationOptions.cs | [ ] | | + +--- + +## TYPES + +| File | Status | Notes | +|------|--------|-------| +| ToggledEventArgs.cs | [x] | ADDED - was missing, required by SkiaSwitch | diff --git a/Types/ToggledEventArgs.cs b/Types/ToggledEventArgs.cs new file mode 100644 index 0000000..54ac8b1 --- /dev/null +++ b/Types/ToggledEventArgs.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.Maui.Platform; + +public class ToggledEventArgs : EventArgs +{ + public bool Value { get; } + + public ToggledEventArgs(bool value) + { + Value = value; + } +} diff --git a/Views/CheckedChangedEventArgs.cs b/Views/CheckedChangedEventArgs.cs new file mode 100644 index 0000000..001dce4 --- /dev/null +++ b/Views/CheckedChangedEventArgs.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.Maui.Platform; + +public class CheckedChangedEventArgs : EventArgs +{ + public bool IsChecked { get; } + + public CheckedChangedEventArgs(bool isChecked) + { + IsChecked = isChecked; + } +} diff --git a/Views/SkiaActivityIndicator.cs b/Views/SkiaActivityIndicator.cs index bdf374c..cbe554d 100644 --- a/Views/SkiaActivityIndicator.cs +++ b/Views/SkiaActivityIndicator.cs @@ -1,6 +1,7 @@ // 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 SkiaSharp; namespace Microsoft.Maui.Platform; @@ -21,6 +22,7 @@ public class SkiaActivityIndicator : SkiaView typeof(bool), typeof(SkiaActivityIndicator), false, + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaActivityIndicator)b).OnIsRunningChanged()); /// @@ -32,6 +34,7 @@ public class SkiaActivityIndicator : SkiaView typeof(SKColor), typeof(SkiaActivityIndicator), new SKColor(0x21, 0x96, 0xF3), + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaActivityIndicator)b).Invalidate()); /// @@ -43,6 +46,7 @@ public class SkiaActivityIndicator : SkiaView typeof(SKColor), typeof(SkiaActivityIndicator), new SKColor(0xBD, 0xBD, 0xBD), + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaActivityIndicator)b).Invalidate()); /// @@ -54,6 +58,7 @@ public class SkiaActivityIndicator : SkiaView typeof(float), typeof(SkiaActivityIndicator), 32f, + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaActivityIndicator)b).InvalidateMeasure()); /// @@ -65,6 +70,7 @@ public class SkiaActivityIndicator : SkiaView typeof(float), typeof(SkiaActivityIndicator), 3f, + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaActivityIndicator)b).InvalidateMeasure()); /// @@ -75,7 +81,8 @@ public class SkiaActivityIndicator : SkiaView nameof(RotationSpeed), typeof(float), typeof(SkiaActivityIndicator), - 360f); + 360f, + BindingMode.TwoWay); /// /// Bindable property for ArcCount. @@ -86,6 +93,7 @@ public class SkiaActivityIndicator : SkiaView typeof(int), typeof(SkiaActivityIndicator), 12, + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaActivityIndicator)b).Invalidate()); #endregion diff --git a/Views/SkiaBorder.cs b/Views/SkiaBorder.cs index 79b5268..11a157f 100644 --- a/Views/SkiaBorder.cs +++ b/Views/SkiaBorder.cs @@ -1,6 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; +using Microsoft.Maui.Controls; +using Microsoft.Maui.Platform.Linux.Handlers; using SkiaSharp; namespace Microsoft.Maui.Platform; @@ -14,54 +17,56 @@ public class SkiaBorder : SkiaLayoutView public static readonly BindableProperty StrokeThicknessProperty = BindableProperty.Create(nameof(StrokeThickness), typeof(float), typeof(SkiaBorder), 1f, - propertyChanged: (b, o, n) => ((SkiaBorder)b).Invalidate()); + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaBorder)b).Invalidate()); public static readonly BindableProperty CornerRadiusProperty = BindableProperty.Create(nameof(CornerRadius), typeof(float), typeof(SkiaBorder), 0f, - propertyChanged: (b, o, n) => ((SkiaBorder)b).Invalidate()); + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaBorder)b).Invalidate()); public static readonly BindableProperty StrokeProperty = BindableProperty.Create(nameof(Stroke), typeof(SKColor), typeof(SkiaBorder), SKColors.Black, - propertyChanged: (b, o, n) => ((SkiaBorder)b).Invalidate()); + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaBorder)b).Invalidate()); public static readonly BindableProperty PaddingLeftProperty = BindableProperty.Create(nameof(PaddingLeft), typeof(float), typeof(SkiaBorder), 0f, - propertyChanged: (b, o, n) => ((SkiaBorder)b).InvalidateMeasure()); + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaBorder)b).InvalidateMeasure()); public static readonly BindableProperty PaddingTopProperty = BindableProperty.Create(nameof(PaddingTop), typeof(float), typeof(SkiaBorder), 0f, - propertyChanged: (b, o, n) => ((SkiaBorder)b).InvalidateMeasure()); + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaBorder)b).InvalidateMeasure()); public static readonly BindableProperty PaddingRightProperty = BindableProperty.Create(nameof(PaddingRight), typeof(float), typeof(SkiaBorder), 0f, - propertyChanged: (b, o, n) => ((SkiaBorder)b).InvalidateMeasure()); + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaBorder)b).InvalidateMeasure()); public static readonly BindableProperty PaddingBottomProperty = BindableProperty.Create(nameof(PaddingBottom), typeof(float), typeof(SkiaBorder), 0f, - propertyChanged: (b, o, n) => ((SkiaBorder)b).InvalidateMeasure()); + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaBorder)b).InvalidateMeasure()); public static readonly BindableProperty HasShadowProperty = BindableProperty.Create(nameof(HasShadow), typeof(bool), typeof(SkiaBorder), false, - propertyChanged: (b, o, n) => ((SkiaBorder)b).Invalidate()); + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaBorder)b).Invalidate()); 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()); + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaBorder)b).Invalidate()); public static readonly BindableProperty ShadowBlurRadiusProperty = BindableProperty.Create(nameof(ShadowBlurRadius), typeof(float), typeof(SkiaBorder), 4f, - propertyChanged: (b, o, n) => ((SkiaBorder)b).Invalidate()); + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaBorder)b).Invalidate()); public static readonly BindableProperty ShadowOffsetXProperty = BindableProperty.Create(nameof(ShadowOffsetX), typeof(float), typeof(SkiaBorder), 2f, - propertyChanged: (b, o, n) => ((SkiaBorder)b).Invalidate()); + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaBorder)b).Invalidate()); public static readonly BindableProperty ShadowOffsetYProperty = BindableProperty.Create(nameof(ShadowOffsetY), typeof(float), typeof(SkiaBorder), 2f, - propertyChanged: (b, o, n) => ((SkiaBorder)b).Invalidate()); + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaBorder)b).Invalidate()); #endregion + private bool _isPressed; + #region Properties public float StrokeThickness @@ -138,6 +143,14 @@ public class SkiaBorder : SkiaLayoutView #endregion + #region Events + + public event EventHandler? Tapped; + + #endregion + + #region SetPadding Methods + /// /// Sets uniform padding on all sides. /// @@ -166,16 +179,18 @@ public class SkiaBorder : SkiaLayoutView PaddingBottom = bottom; } + #endregion + protected override void OnDraw(SKCanvas canvas, SKRect bounds) { var strokeThickness = StrokeThickness; var cornerRadius = CornerRadius; var borderRect = new SKRect( - bounds.Left + strokeThickness / 2, - bounds.Top + strokeThickness / 2, - bounds.Right - strokeThickness / 2, - bounds.Bottom - strokeThickness / 2); + bounds.Left + strokeThickness / 2f, + bounds.Top + strokeThickness / 2f, + bounds.Right - strokeThickness / 2f, + bounds.Bottom - strokeThickness / 2f); // Draw shadow if enabled if (HasShadow) @@ -204,7 +219,7 @@ public class SkiaBorder : SkiaLayoutView canvas.DrawRoundRect(new SKRoundRect(borderRect, cornerRadius), bgPaint); // Draw border - if (strokeThickness > 0) + if (strokeThickness > 0f) { using var borderPaint = new SKPaint { @@ -244,16 +259,16 @@ public class SkiaBorder : SkiaLayoutView protected override SKSize MeasureOverride(SKSize availableSize) { var strokeThickness = StrokeThickness; - var paddingWidth = PaddingLeft + PaddingRight + strokeThickness * 2; - var paddingHeight = PaddingTop + PaddingBottom + strokeThickness * 2; + var paddingWidth = PaddingLeft + PaddingRight + strokeThickness * 2f; + var paddingHeight = PaddingTop + PaddingBottom + strokeThickness * 2f; // Respect explicit size requests - var requestedWidth = WidthRequest >= 0 ? (float)WidthRequest : availableSize.Width; - var requestedHeight = HeightRequest >= 0 ? (float)HeightRequest : availableSize.Height; + var requestedWidth = WidthRequest >= 0.0 ? (float)WidthRequest : availableSize.Width; + var requestedHeight = HeightRequest >= 0.0 ? (float)HeightRequest : availableSize.Height; var childAvailable = new SKSize( - Math.Max(0, requestedWidth - paddingWidth), - Math.Max(0, requestedHeight - paddingHeight)); + Math.Max(0f, requestedWidth - paddingWidth), + Math.Max(0f, requestedHeight - paddingHeight)); var maxChildSize = SKSize.Empty; @@ -266,8 +281,8 @@ public class SkiaBorder : SkiaLayoutView } // 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; + var width = WidthRequest >= 0.0 ? (float)WidthRequest : maxChildSize.Width + paddingWidth; + var height = HeightRequest >= 0.0 ? (float)HeightRequest : maxChildSize.Height + paddingHeight; return new SKSize(width, height); } @@ -290,6 +305,85 @@ public class SkiaBorder : SkiaLayoutView return bounds; } + + private bool HasTapGestureRecognizers() + { + if (MauiView?.GestureRecognizers == null) + { + return false; + } + + foreach (var gestureRecognizer in MauiView.GestureRecognizers) + { + if (gestureRecognizer is TapGestureRecognizer) + { + return true; + } + } + + return false; + } + + public override SkiaView? HitTest(float x, float y) + { + if (IsVisible && IsEnabled) + { + var bounds = Bounds; + if (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; + } + + public override void OnPointerPressed(PointerEventArgs e) + { + if (HasTapGestureRecognizers()) + { + _isPressed = true; + e.Handled = true; + Console.WriteLine("[SkiaBorder] OnPointerPressed INTERCEPTED for gesture, MauiView=" + MauiView?.GetType().Name); + if (MauiView != null) + { + GestureManager.ProcessPointerDown(MauiView, e.X, e.Y); + } + } + else + { + base.OnPointerPressed(e); + } + } + + public override void OnPointerReleased(PointerEventArgs e) + { + if (_isPressed) + { + _isPressed = false; + e.Handled = true; + Console.WriteLine("[SkiaBorder] OnPointerReleased - processing gesture recognizers, MauiView=" + MauiView?.GetType().Name); + if (MauiView != null) + { + GestureManager.ProcessPointerUp(MauiView, e.X, e.Y); + } + Tapped?.Invoke(this, EventArgs.Empty); + } + else + { + base.OnPointerReleased(e); + } + } + + public override void OnPointerExited(PointerEventArgs e) + { + base.OnPointerExited(e); + _isPressed = false; + } } /// diff --git a/Views/SkiaBoxView.cs b/Views/SkiaBoxView.cs index 3a8f100..e33fb0f 100644 --- a/Views/SkiaBoxView.cs +++ b/Views/SkiaBoxView.cs @@ -1,6 +1,7 @@ // 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 SkiaSharp; namespace Microsoft.Maui.Platform; @@ -12,11 +13,11 @@ public class SkiaBoxView : SkiaView { public static readonly BindableProperty ColorProperty = BindableProperty.Create(nameof(Color), typeof(SKColor), typeof(SkiaBoxView), SKColors.Transparent, - propertyChanged: (b, o, n) => ((SkiaBoxView)b).Invalidate()); + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaBoxView)b).Invalidate()); public static readonly BindableProperty CornerRadiusProperty = BindableProperty.Create(nameof(CornerRadius), typeof(float), typeof(SkiaBoxView), 0f, - propertyChanged: (b, o, n) => ((SkiaBoxView)b).Invalidate()); + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaBoxView)b).Invalidate()); public SKColor Color { diff --git a/Views/SkiaButton.cs b/Views/SkiaButton.cs index 45cb8f9..8ee6148 100644 --- a/Views/SkiaButton.cs +++ b/Views/SkiaButton.cs @@ -1,8 +1,11 @@ // 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; +using System.Windows.Input; +using Microsoft.Maui.Controls; using Microsoft.Maui.Platform.Linux.Rendering; +using SkiaSharp; namespace Microsoft.Maui.Platform; @@ -13,443 +16,322 @@ public class SkiaButton : SkiaView { #region BindableProperties - /// - /// 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 TextProperty = BindableProperty.Create( + nameof(Text), + typeof(string), + typeof(SkiaButton), + "", + BindingMode.TwoWay, + propertyChanged: (b, o, n) => ((SkiaButton)b).OnTextChanged()); - /// - /// 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 TextColorProperty = BindableProperty.Create( + nameof(TextColor), + typeof(SKColor), + typeof(SkiaButton), + SKColors.White, + BindingMode.TwoWay, + propertyChanged: (b, o, n) => ((SkiaButton)b).Invalidate()); - /// - /// 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 ButtonBackgroundColorProperty = BindableProperty.Create( + nameof(ButtonBackgroundColor), + typeof(SKColor), + typeof(SkiaButton), + new SKColor(33, 150, 243), // Material Blue + BindingMode.TwoWay, + propertyChanged: (b, o, n) => ((SkiaButton)b).Invalidate()); - /// - /// 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 PressedBackgroundColorProperty = BindableProperty.Create( + nameof(PressedBackgroundColor), + typeof(SKColor), + typeof(SkiaButton), + new SKColor(25, 118, 210), + BindingMode.TwoWay, + propertyChanged: (b, o, n) => ((SkiaButton)b).Invalidate()); - /// - /// 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 DisabledBackgroundColorProperty = BindableProperty.Create( + nameof(DisabledBackgroundColor), + typeof(SKColor), + typeof(SkiaButton), + new SKColor(189, 189, 189), + BindingMode.TwoWay, + propertyChanged: (b, o, n) => ((SkiaButton)b).Invalidate()); - /// - /// 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 HoveredBackgroundColorProperty = BindableProperty.Create( + nameof(HoveredBackgroundColor), + typeof(SKColor), + typeof(SkiaButton), + new SKColor(66, 165, 245), + BindingMode.TwoWay, + propertyChanged: (b, o, n) => ((SkiaButton)b).Invalidate()); - /// - /// 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 BorderColorProperty = BindableProperty.Create( + nameof(BorderColor), + typeof(SKColor), + typeof(SkiaButton), + SKColors.Transparent, + BindingMode.TwoWay, + propertyChanged: (b, o, n) => ((SkiaButton)b).Invalidate()); - /// - /// 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 FontFamilyProperty = BindableProperty.Create( + nameof(FontFamily), + typeof(string), + typeof(SkiaButton), + "Sans", + BindingMode.TwoWay, + propertyChanged: (b, o, n) => ((SkiaButton)b).OnFontChanged()); - /// - /// 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 FontSizeProperty = BindableProperty.Create( + nameof(FontSize), + typeof(float), + typeof(SkiaButton), + 14f, + BindingMode.TwoWay, + propertyChanged: (b, o, n) => ((SkiaButton)b).OnFontChanged()); - /// - /// 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 IsBoldProperty = BindableProperty.Create( + nameof(IsBold), + typeof(bool), + typeof(SkiaButton), + false, + BindingMode.TwoWay, + propertyChanged: (b, o, n) => ((SkiaButton)b).OnFontChanged()); - /// - /// 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 IsItalicProperty = BindableProperty.Create( + nameof(IsItalic), + typeof(bool), + typeof(SkiaButton), + false, + BindingMode.TwoWay, + propertyChanged: (b, o, n) => ((SkiaButton)b).OnFontChanged()); - /// - /// 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 CharacterSpacingProperty = BindableProperty.Create( + nameof(CharacterSpacing), + typeof(float), + typeof(SkiaButton), + 0f, + BindingMode.TwoWay, + propertyChanged: (b, o, n) => ((SkiaButton)b).Invalidate()); - /// - /// 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 CornerRadiusProperty = BindableProperty.Create( + nameof(CornerRadius), + typeof(float), + typeof(SkiaButton), + 4f, + BindingMode.TwoWay, + propertyChanged: (b, o, n) => ((SkiaButton)b).Invalidate()); - /// - /// 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 BorderWidthProperty = BindableProperty.Create( + nameof(BorderWidth), + typeof(float), + typeof(SkiaButton), + 0f, + BindingMode.TwoWay, + propertyChanged: (b, o, n) => ((SkiaButton)b).Invalidate()); - /// - /// 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 PaddingProperty = BindableProperty.Create( + nameof(Padding), + typeof(SKRect), + typeof(SkiaButton), + new SKRect(16f, 8f, 16f, 8f), + BindingMode.TwoWay, + propertyChanged: (b, o, n) => ((SkiaButton)b).InvalidateMeasure()); - /// - /// 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 CommandProperty = BindableProperty.Create( + nameof(Command), + typeof(ICommand), + typeof(SkiaButton), + null, + BindingMode.TwoWay, + propertyChanged: (b, o, n) => ((SkiaButton)b).OnCommandChanged((ICommand?)o, (ICommand?)n)); - /// - /// Bindable property for CommandParameter. - /// - public static readonly BindableProperty CommandParameterProperty = - BindableProperty.Create( - nameof(CommandParameter), - typeof(object), - typeof(SkiaButton), - null); + public static readonly BindableProperty CommandParameterProperty = BindableProperty.Create( + nameof(CommandParameter), + typeof(object), + typeof(SkiaButton), + null, + BindingMode.TwoWay); - /// - /// Bindable property for ImageSource. - /// - public static readonly BindableProperty ImageSourceProperty = - BindableProperty.Create( - nameof(ImageSource), - typeof(SKBitmap), - typeof(SkiaButton), - null, - propertyChanged: (b, o, n) => ((SkiaButton)b).Invalidate()); + public static readonly BindableProperty ImageSourceProperty = BindableProperty.Create( + nameof(ImageSource), + typeof(SKBitmap), + typeof(SkiaButton), + null, + BindingMode.TwoWay, + propertyChanged: (b, o, n) => ((SkiaButton)b).Invalidate()); - /// - /// Bindable property for ImageSpacing. - /// - public static readonly BindableProperty ImageSpacingProperty = - BindableProperty.Create( - nameof(ImageSpacing), - typeof(float), - typeof(SkiaButton), - 8f, - propertyChanged: (b, o, n) => ((SkiaButton)b).InvalidateMeasure()); + public static readonly BindableProperty ImageSpacingProperty = BindableProperty.Create( + nameof(ImageSpacing), + typeof(float), + typeof(SkiaButton), + 8f, + BindingMode.TwoWay, + propertyChanged: (b, o, n) => ((SkiaButton)b).InvalidateMeasure()); - /// - /// Bindable property for ContentLayoutPosition (0=Left, 1=Top, 2=Right, 3=Bottom). - /// - public static readonly BindableProperty ContentLayoutPositionProperty = - BindableProperty.Create( - nameof(ContentLayoutPosition), - typeof(int), - typeof(SkiaButton), - 0, - propertyChanged: (b, o, n) => ((SkiaButton)b).InvalidateMeasure()); + public static readonly BindableProperty ContentLayoutPositionProperty = BindableProperty.Create( + nameof(ContentLayoutPosition), + typeof(int), + typeof(SkiaButton), + 0, + BindingMode.TwoWay, + propertyChanged: (b, o, n) => ((SkiaButton)b).InvalidateMeasure()); + + #endregion + + #region Fields + + private bool _focusFromKeyboard; #endregion #region Properties - /// - /// Gets or sets the button text. - /// public string Text { get => (string)GetValue(TextProperty); set => SetValue(TextProperty, value); } - /// - /// Gets or sets the text color. - /// public SKColor TextColor { get => (SKColor)GetValue(TextColorProperty); set => SetValue(TextColorProperty, value); } - /// - /// Gets or sets the button background color. - /// public SKColor ButtonBackgroundColor { get => (SKColor)GetValue(ButtonBackgroundColorProperty); set => SetValue(ButtonBackgroundColorProperty, value); } - /// - /// Gets or sets the pressed background color. - /// public SKColor PressedBackgroundColor { get => (SKColor)GetValue(PressedBackgroundColorProperty); set => SetValue(PressedBackgroundColorProperty, value); } - /// - /// Gets or sets the disabled background color. - /// public SKColor DisabledBackgroundColor { get => (SKColor)GetValue(DisabledBackgroundColorProperty); set => SetValue(DisabledBackgroundColorProperty, value); } - /// - /// Gets or sets the hovered background color. - /// public SKColor HoveredBackgroundColor { get => (SKColor)GetValue(HoveredBackgroundColorProperty); set => SetValue(HoveredBackgroundColorProperty, value); } - /// - /// Gets or sets the border color. - /// public SKColor BorderColor { get => (SKColor)GetValue(BorderColorProperty); set => SetValue(BorderColorProperty, 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 character spacing. - /// public float CharacterSpacing { get => (float)GetValue(CharacterSpacingProperty); set => SetValue(CharacterSpacingProperty, 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 the command to execute when clicked. - /// - public System.Windows.Input.ICommand? Command + public ICommand? Command { - get => (System.Windows.Input.ICommand?)GetValue(CommandProperty); + get => (ICommand?)GetValue(CommandProperty); set => SetValue(CommandProperty, value); } - /// - /// Gets or sets the command parameter. - /// public object? CommandParameter { get => GetValue(CommandParameterProperty); set => SetValue(CommandParameterProperty, value); } - /// - /// Gets or sets the image source for the button. - /// public SKBitmap? ImageSource { get => (SKBitmap?)GetValue(ImageSourceProperty); set => SetValue(ImageSourceProperty, value); } - /// - /// Gets or sets the spacing between the image and text. - /// public float ImageSpacing { get => (float)GetValue(ImageSpacingProperty); set => SetValue(ImageSpacingProperty, value); } - /// - /// Gets or sets the content layout position (0=Left, 1=Top, 2=Right, 3=Bottom). - /// public int ContentLayoutPosition { get => (int)GetValue(ContentLayoutPositionProperty); set => SetValue(ContentLayoutPositionProperty, value); } - /// - /// Gets whether the button is currently pressed. - /// public bool IsPressed { get; private set; } - /// - /// Gets whether the pointer is currently over the button. - /// public bool IsHovered { get; private set; } #endregion - private bool _focusFromKeyboard; + #region Events - /// - /// Event raised when the button is clicked. - /// public event EventHandler? Clicked; - /// - /// Event raised when the button is pressed. - /// public event EventHandler? Pressed; - /// - /// Event raised when the button is released. - /// public event EventHandler? Released; + #endregion + + #region Constructor + public SkiaButton() { IsFocusable = true; } + #endregion + + #region Private Methods + private void OnTextChanged() { InvalidateMeasure(); @@ -462,13 +344,12 @@ public class SkiaButton : SkiaView Invalidate(); } - private void OnCommandChanged(System.Windows.Input.ICommand? oldCommand, System.Windows.Input.ICommand? newCommand) + private void OnCommandChanged(ICommand? oldCommand, ICommand? newCommand) { if (oldCommand != null) { oldCommand.CanExecuteChanged -= OnCanExecuteChanged; } - if (newCommand != null) { newCommand.CanExecuteChanged += OnCanExecuteChanged; @@ -489,55 +370,52 @@ public class SkiaButton : SkiaView } } + #endregion + + #region Drawing + protected override void OnDraw(SKCanvas canvas, SKRect bounds) { - // Check if this is a "text only" button (transparent background) - var isTextOnly = ButtonBackgroundColor.Alpha == 0; + SKColor buttonBackgroundColor = ButtonBackgroundColor; + bool isTextOnly = buttonBackgroundColor.Alpha == 0; - // Determine background color based on state - SKColor bgColor; + SKColor color; if (!IsEnabled) { - bgColor = isTextOnly ? SKColors.Transparent : DisabledBackgroundColor; + color = 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; + color = 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; + color = isTextOnly ? new SKColor(0, 0, 0, 10) : HoveredBackgroundColor; } else { - bgColor = ButtonBackgroundColor; + color = ButtonBackgroundColor; } - // Draw shadow (for elevation effect) - skip for text-only buttons if (IsEnabled && !IsPressed && !isTextOnly) { DrawShadow(canvas, bounds); } - // Create rounded rect for background and border - var rect = new SKRoundRect(bounds, CornerRadius); + var roundRect = new SKRoundRect(bounds, CornerRadius); - // Draw background with rounded corners (skip if fully transparent) - if (bgColor.Alpha > 0) + if (color.Alpha > 0) { using var bgPaint = new SKPaint { - Color = bgColor, + Color = color, IsAntialias = true, Style = SKPaintStyle.Fill }; - canvas.DrawRoundRect(rect, bgPaint); + canvas.DrawRoundRect(roundRect, bgPaint); } - // Draw border - if (BorderWidth > 0 && BorderColor != SKColors.Transparent) + if (BorderWidth > 0f && BorderColor != SKColors.Transparent) { using var borderPaint = new SKPaint { @@ -546,40 +424,37 @@ public class SkiaButton : SkiaView Style = SKPaintStyle.Stroke, StrokeWidth = BorderWidth }; - canvas.DrawRoundRect(rect, borderPaint); + canvas.DrawRoundRect(roundRect, borderPaint); } - // Draw focus ring only for keyboard focus if (IsFocused && _focusFromKeyboard) { using var focusPaint = new SKPaint { - Color = new SKColor(0x21, 0x96, 0xF3, 0x80), + Color = new SKColor(33, 150, 243, 128), IsAntialias = true, Style = SKPaintStyle.Stroke, - StrokeWidth = 2 + StrokeWidth = 2f }; - var focusRect = new SKRoundRect(bounds, CornerRadius + 2); - focusRect.Inflate(2, 2); + var focusRect = new SKRoundRect(bounds, CornerRadius + 2f); + focusRect.Inflate(2f, 2f); canvas.DrawRoundRect(focusRect, focusPaint); } - // Draw content (text and/or image) DrawContent(canvas, bounds, isTextOnly); } private void DrawContent(SKCanvas canvas, SKRect bounds, bool isTextOnly) { - var fontStyle = new SKFontStyle( + var style = 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 font = new SKFont( + SkiaRenderingEngine.Current?.ResourceCache.GetTypeface(FontFamily, style) ?? SKTypeface.Default, + FontSize, 1f, 0f); - // Determine text color SKColor textColorToUse; if (!IsEnabled) { @@ -604,7 +479,6 @@ public class SkiaButton : SkiaView IsAntialias = true }; - // Measure text var textBounds = new SKRect(); bool hasText = !string.IsNullOrEmpty(Text); if (hasText) @@ -612,21 +486,21 @@ public class SkiaButton : SkiaView textPaint.MeasureText(Text, ref textBounds); } - // Calculate image size bool hasImage = ImageSource != null; - float imageWidth = 0; - float imageHeight = 0; + float imageWidth = 0f; + float imageHeight = 0f; if (hasImage) { - float maxImageSize = Math.Min(bounds.Height - 8, 24f); - float scale = Math.Min(maxImageSize / ImageSource!.Width, maxImageSize / ImageSource.Height); + float maxSize = Math.Min(bounds.Height - 8f, 24f); + float scale = Math.Min(maxSize / ImageSource!.Width, maxSize / ImageSource.Height); imageWidth = ImageSource.Width * scale; imageHeight = ImageSource.Height * scale; } - // Calculate total content size and position bool isHorizontal = ContentLayoutPosition == 0 || ContentLayoutPosition == 2; - float totalWidth, totalHeight; + float totalWidth; + float totalHeight; + if (hasImage && hasText) { if (isHorizontal) @@ -651,63 +525,66 @@ public class SkiaButton : SkiaView totalHeight = textBounds.Height; } - float startX = bounds.MidX - totalWidth / 2; - float startY = bounds.MidY - totalHeight / 2; + float startX = bounds.MidX - totalWidth / 2f; + float startY = bounds.MidY - totalHeight / 2f; - // Draw image and text based on layout position if (hasImage) { - float imageX, imageY; - float textX = 0, textY = 0; + float imageX; + float imageY; + float textX = 0f; + float textY = 0f; switch (ContentLayoutPosition) { - case 1: // Top - image above text - imageX = bounds.MidX - imageWidth / 2; + case 1: // Top + imageX = bounds.MidX - imageWidth / 2f; imageY = startY; - textX = bounds.MidX - textBounds.Width / 2; + textX = bounds.MidX - textBounds.Width / 2f; textY = startY + imageHeight + ImageSpacing - textBounds.Top; break; - case 2: // Right - image to right of text + case 2: // Right textX = startX; textY = bounds.MidY - textBounds.MidY; imageX = startX + textBounds.Width + ImageSpacing; - imageY = bounds.MidY - imageHeight / 2; + imageY = bounds.MidY - imageHeight / 2f; break; - case 3: // Bottom - image below text - textX = bounds.MidX - textBounds.Width / 2; + case 3: // Bottom + textX = bounds.MidX - textBounds.Width / 2f; textY = startY - textBounds.Top; - imageX = bounds.MidX - imageWidth / 2; + imageX = bounds.MidX - imageWidth / 2f; imageY = startY + textBounds.Height + ImageSpacing; break; - default: // 0 = Left - image to left of text + default: // 0 = Left imageX = startX; - imageY = bounds.MidY - imageHeight / 2; + imageY = bounds.MidY - imageHeight / 2f; textX = startX + imageWidth + ImageSpacing; textY = bounds.MidY - textBounds.MidY; break; } - // Draw image var imageRect = new SKRect(imageX, imageY, imageX + imageWidth, imageY + imageHeight); using var imagePaint = new SKPaint { IsAntialias = true }; + if (!IsEnabled) { - imagePaint.ColorFilter = SKColorFilter.CreateBlendMode(new SKColor(128, 128, 128, 128), SKBlendMode.Modulate); + imagePaint.ColorFilter = SKColorFilter.CreateBlendMode( + new SKColor(128, 128, 128, 128), SKBlendMode.Modulate); } + canvas.DrawBitmap(ImageSource!, imageRect, imagePaint); - // Draw text if (hasText) { canvas.DrawText(Text!, textX, textY, textPaint); } + return; } - else if (hasText) + + if (hasText) { - // Just text, centered - var x = bounds.MidX - textBounds.MidX; - var y = bounds.MidY - textBounds.MidY; + float x = bounds.MidX - textBounds.MidX; + float y = bounds.MidY - textBounds.MidY; canvas.DrawText(Text!, x, y, textPaint); } } @@ -718,25 +595,27 @@ public class SkiaButton : SkiaView { Color = new SKColor(0, 0, 0, 50), IsAntialias = true, - MaskFilter = SKMaskFilter.CreateBlur(SKBlurStyle.Normal, 4) + MaskFilter = SKMaskFilter.CreateBlur(SKBlurStyle.Normal, 4f) }; - 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); + var shadowRect = new SKRoundRect( + new SKRect(bounds.Left + 2f, bounds.Top + 4f, bounds.Right + 2f, bounds.Bottom + 4f), + CornerRadius); + canvas.DrawRoundRect(shadowRect, shadowPaint); } + #endregion + + #region Pointer Events + public override void OnPointerEntered(PointerEventArgs e) { - if (!IsEnabled) return; - IsHovered = true; - SkiaVisualStateManager.GoToState(this, SkiaVisualStateManager.CommonStates.PointerOver); - Invalidate(); + if (IsEnabled) + { + IsHovered = true; + SkiaVisualStateManager.GoToState(this, "PointerOver"); + Invalidate(); + } } public override void OnPointerExited(PointerEventArgs e) @@ -746,53 +625,51 @@ public class SkiaButton : SkiaView { IsPressed = false; } - SkiaVisualStateManager.GoToState(this, IsEnabled ? SkiaVisualStateManager.CommonStates.Normal : SkiaVisualStateManager.CommonStates.Disabled); + SkiaVisualStateManager.GoToState(this, IsEnabled ? "Normal" : "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); + if (IsEnabled) + { + IsPressed = true; + _focusFromKeyboard = false; + SkiaVisualStateManager.GoToState(this, "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) + if (IsEnabled) { - Clicked?.Invoke(this, EventArgs.Empty); - Command?.Execute(CommandParameter); + bool wasPressed = IsPressed; + IsPressed = false; + SkiaVisualStateManager.GoToState(this, IsHovered ? "PointerOver" : "Normal"); + Invalidate(); + Released?.Invoke(this, EventArgs.Empty); + if (wasPressed) + { + Clicked?.Invoke(this, EventArgs.Empty); + Command?.Execute(CommandParameter); + } } } + #endregion + + #region Keyboard Events + public override void OnKeyDown(KeyEventArgs e) { - if (!IsEnabled) return; - - // Activate on Enter or Space - if (e.Key == Key.Enter || e.Key == Key.Space) + if (IsEnabled && (e.Key == Key.Enter || e.Key == Key.Space)) { IsPressed = true; _focusFromKeyboard = true; - SkiaVisualStateManager.GoToState(this, SkiaVisualStateManager.CommonStates.Pressed); + SkiaVisualStateManager.GoToState(this, "Pressed"); Invalidate(); Pressed?.Invoke(this, EventArgs.Empty); e.Handled = true; @@ -801,14 +678,12 @@ public class SkiaButton : SkiaView public override void OnKeyUp(KeyEventArgs e) { - if (!IsEnabled) return; - - if (e.Key == Key.Enter || e.Key == Key.Space) + if (IsEnabled && (e.Key == Key.Enter || e.Key == Key.Space)) { if (IsPressed) { IsPressed = false; - SkiaVisualStateManager.GoToState(this, SkiaVisualStateManager.CommonStates.Normal); + SkiaVisualStateManager.GoToState(this, "Normal"); Invalidate(); Released?.Invoke(this, EventArgs.Empty); Clicked?.Invoke(this, EventArgs.Empty); @@ -818,54 +693,70 @@ public class SkiaButton : SkiaView } } + #endregion + + #region State Changes + protected override void OnEnabledChanged() { base.OnEnabledChanged(); - SkiaVisualStateManager.GoToState(this, IsEnabled ? SkiaVisualStateManager.CommonStates.Normal : SkiaVisualStateManager.CommonStates.Disabled); + SkiaVisualStateManager.GoToState(this, IsEnabled ? "Normal" : "Disabled"); } + #endregion + + #region Measurement + 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; + SKRect padding = Padding; + float paddingLeft = float.IsNaN(padding.Left) ? 16f : padding.Left; + float paddingRight = float.IsNaN(padding.Right) ? 16f : padding.Right; + float paddingTop = float.IsNaN(padding.Top) ? 8f : padding.Top; + float paddingBottom = float.IsNaN(padding.Bottom) ? 8f : padding.Bottom; + float fontSize = (float.IsNaN(FontSize) || FontSize <= 0f) ? 14f : FontSize; if (string.IsNullOrEmpty(Text)) { - return new SKSize( - paddingLeft + paddingRight + 40, // Minimum width - paddingTop + paddingBottom + fontSize); + return new SKSize(paddingLeft + paddingRight + 40f, 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; + var style = new SKFontStyle( + IsBold ? SKFontStyleWeight.Bold : SKFontStyleWeight.Normal, + SKFontStyleWidth.Normal, + IsItalic ? SKFontStyleSlant.Italic : SKFontStyleSlant.Upright); + + using var font = new SKFont( + SkiaRenderingEngine.Current?.ResourceCache.GetTypeface(FontFamily, style) ?? SKTypeface.Default, + fontSize, 1f, 0f); - 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; + float width = textBounds.Width + paddingLeft + paddingRight; + float 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; + if (float.IsNaN(width) || width < 0f) + { + width = 72f; + } + if (float.IsNaN(height) || height < 0f) + { + height = 30f; + } - // Respect WidthRequest and HeightRequest when set - if (WidthRequest >= 0) + if (WidthRequest >= 0.0) + { width = (float)WidthRequest; - if (HeightRequest >= 0) + } + if (HeightRequest >= 0.0) + { height = (float)HeightRequest; + } return new SKSize(width, height); } + + #endregion } diff --git a/Views/SkiaCheckBox.cs b/Views/SkiaCheckBox.cs index 7fddd92..222ae11 100644 --- a/Views/SkiaCheckBox.cs +++ b/Views/SkiaCheckBox.cs @@ -1,8 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; +using Microsoft.Maui.Controls; using SkiaSharp; -using Microsoft.Maui.Platform.Linux.Rendering; namespace Microsoft.Maui.Platform; @@ -13,241 +14,179 @@ public class SkiaCheckBox : SkiaView { #region BindableProperties - /// - /// Bindable property for IsChecked. - /// public static readonly BindableProperty IsCheckedProperty = BindableProperty.Create( nameof(IsChecked), typeof(bool), typeof(SkiaCheckBox), false, - BindingMode.TwoWay, + BindingMode.OneWay, propertyChanged: (b, o, n) => ((SkiaCheckBox)b).OnIsCheckedChanged()); - /// - /// Bindable property for CheckColor. - /// public static readonly BindableProperty CheckColorProperty = BindableProperty.Create( nameof(CheckColor), typeof(SKColor), typeof(SkiaCheckBox), SKColors.White, + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaCheckBox)b).Invalidate()); - /// - /// Bindable property for BoxColor. - /// public static readonly BindableProperty BoxColorProperty = BindableProperty.Create( nameof(BoxColor), typeof(SKColor), typeof(SkiaCheckBox), - new SKColor(0x21, 0x96, 0xF3), + new SKColor(33, 150, 243), + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaCheckBox)b).Invalidate()); - /// - /// Bindable property for UncheckedBoxColor. - /// public static readonly BindableProperty UncheckedBoxColorProperty = BindableProperty.Create( nameof(UncheckedBoxColor), typeof(SKColor), typeof(SkiaCheckBox), SKColors.White, + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaCheckBox)b).Invalidate()); - /// - /// Bindable property for BorderColor. - /// public static readonly BindableProperty BorderColorProperty = BindableProperty.Create( nameof(BorderColor), typeof(SKColor), typeof(SkiaCheckBox), - new SKColor(0x75, 0x75, 0x75), + new SKColor(117, 117, 117), + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaCheckBox)b).Invalidate()); - /// - /// Bindable property for DisabledColor. - /// public static readonly BindableProperty DisabledColorProperty = BindableProperty.Create( nameof(DisabledColor), typeof(SKColor), typeof(SkiaCheckBox), - new SKColor(0xBD, 0xBD, 0xBD), + new SKColor(189, 189, 189), + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaCheckBox)b).Invalidate()); - /// - /// Bindable property for HoveredBorderColor. - /// public static readonly BindableProperty HoveredBorderColorProperty = BindableProperty.Create( nameof(HoveredBorderColor), typeof(SKColor), typeof(SkiaCheckBox), - new SKColor(0x21, 0x96, 0xF3), + new SKColor(33, 150, 243), + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaCheckBox)b).Invalidate()); - /// - /// Bindable property for BoxSize. - /// public static readonly BindableProperty BoxSizeProperty = BindableProperty.Create( nameof(BoxSize), typeof(float), typeof(SkiaCheckBox), 20f, + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaCheckBox)b).InvalidateMeasure()); - /// - /// Bindable property for CornerRadius. - /// public static readonly BindableProperty CornerRadiusProperty = BindableProperty.Create( nameof(CornerRadius), typeof(float), typeof(SkiaCheckBox), 3f, + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaCheckBox)b).Invalidate()); - /// - /// Bindable property for BorderWidth. - /// public static readonly BindableProperty BorderWidthProperty = BindableProperty.Create( nameof(BorderWidth), typeof(float), typeof(SkiaCheckBox), 2f, + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaCheckBox)b).Invalidate()); - /// - /// Bindable property for CheckStrokeWidth. - /// public static readonly BindableProperty CheckStrokeWidthProperty = BindableProperty.Create( nameof(CheckStrokeWidth), typeof(float), typeof(SkiaCheckBox), 2.5f, + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaCheckBox)b).Invalidate()); #endregion #region Properties - /// - /// Gets or sets whether the checkbox is checked. - /// public bool IsChecked { get => (bool)GetValue(IsCheckedProperty); set => SetValue(IsCheckedProperty, value); } - /// - /// Gets or sets the check color. - /// public SKColor CheckColor { get => (SKColor)GetValue(CheckColorProperty); set => SetValue(CheckColorProperty, value); } - /// - /// Gets or sets the box color when checked. - /// public SKColor BoxColor { get => (SKColor)GetValue(BoxColorProperty); set => SetValue(BoxColorProperty, value); } - /// - /// Gets or sets the box color when unchecked. - /// public SKColor UncheckedBoxColor { get => (SKColor)GetValue(UncheckedBoxColorProperty); set => SetValue(UncheckedBoxColorProperty, value); } - /// - /// Gets or sets the border color. - /// public SKColor BorderColor { get => (SKColor)GetValue(BorderColorProperty); set => SetValue(BorderColorProperty, value); } - /// - /// Gets or sets the disabled color. - /// public SKColor DisabledColor { get => (SKColor)GetValue(DisabledColorProperty); set => SetValue(DisabledColorProperty, value); } - /// - /// Gets or sets the hovered border color. - /// public SKColor HoveredBorderColor { get => (SKColor)GetValue(HoveredBorderColorProperty); set => SetValue(HoveredBorderColorProperty, value); } - /// - /// Gets or sets the box size. - /// public float BoxSize { get => (float)GetValue(BoxSizeProperty); set => SetValue(BoxSizeProperty, 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 check stroke width. - /// public float CheckStrokeWidth { get => (float)GetValue(CheckStrokeWidthProperty); set => SetValue(CheckStrokeWidthProperty, value); } - /// - /// Gets whether the pointer is over the checkbox. - /// public bool IsHovered { get; private set; } #endregion - /// - /// Event raised when checked state changes. - /// public event EventHandler? CheckedChanged; public SkiaCheckBox() @@ -258,7 +197,7 @@ public class SkiaCheckBox : SkiaView private void OnIsCheckedChanged() { CheckedChanged?.Invoke(this, new CheckedChangedEventArgs(IsChecked)); - SkiaVisualStateManager.GoToState(this, IsChecked ? SkiaVisualStateManager.CommonStates.Checked : SkiaVisualStateManager.CommonStates.Unchecked); + SkiaVisualStateManager.GoToState(this, IsChecked ? "Checked" : "Unchecked"); Invalidate(); } @@ -266,13 +205,19 @@ public class SkiaCheckBox : SkiaView { // 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); + bounds.Left + (bounds.Width - BoxSize) / 2f, + bounds.Top + (bounds.Height - BoxSize) / 2f, + bounds.Left + (bounds.Width - BoxSize) / 2f + BoxSize, + bounds.Top + (bounds.Height - BoxSize) / 2f + BoxSize); var roundRect = new SKRoundRect(boxRect, CornerRadius); + // Debug logging when checked + if (IsChecked) + { + Console.WriteLine($"[SkiaCheckBox] OnDraw CHECKED - BoxColor=({BoxColor.Red},{BoxColor.Green},{BoxColor.Blue}), UncheckedBoxColor=({UncheckedBoxColor.Red},{UncheckedBoxColor.Green},{UncheckedBoxColor.Blue})"); + } + // Draw background using var bgPaint = new SKPaint { @@ -305,10 +250,10 @@ public class SkiaCheckBox : SkiaView Color = BoxColor.WithAlpha(80), IsAntialias = true, Style = SKPaintStyle.Stroke, - StrokeWidth = 3 + StrokeWidth = 3f }; var focusRect = new SKRoundRect(boxRect, CornerRadius); - focusRect.Inflate(4, 4); + focusRect.Inflate(4f, 4f); canvas.DrawRoundRect(focusRect, focusPaint); } @@ -323,7 +268,7 @@ public class SkiaCheckBox : SkiaView { using var paint = new SKPaint { - Color = CheckColor, + Color = SKColors.White, IsAntialias = true, Style = SKPaintStyle.Stroke, StrokeWidth = CheckStrokeWidth, @@ -331,14 +276,12 @@ public class SkiaCheckBox : SkiaView 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; + float padding = BoxSize * 0.2f; + float left = boxRect.Left + padding; + float right = boxRect.Right - padding; + float top = boxRect.Top + padding; + float 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); @@ -349,37 +292,37 @@ public class SkiaCheckBox : SkiaView public override void OnPointerEntered(PointerEventArgs e) { - if (!IsEnabled) return; - IsHovered = true; - SkiaVisualStateManager.GoToState(this, SkiaVisualStateManager.CommonStates.PointerOver); - Invalidate(); + if (IsEnabled) + { + IsHovered = true; + SkiaVisualStateManager.GoToState(this, "PointerOver"); + Invalidate(); + } } public override void OnPointerExited(PointerEventArgs e) { IsHovered = false; - SkiaVisualStateManager.GoToState(this, IsEnabled ? SkiaVisualStateManager.CommonStates.Normal : SkiaVisualStateManager.CommonStates.Disabled); + SkiaVisualStateManager.GoToState(this, IsEnabled ? "Normal" : "Disabled"); Invalidate(); } public override void OnPointerPressed(PointerEventArgs e) { - if (!IsEnabled) return; - IsChecked = !IsChecked; - e.Handled = true; + if (IsEnabled) + { + 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) + if (IsEnabled && e.Key == Key.Space) { IsChecked = !IsChecked; e.Handled = true; @@ -389,25 +332,11 @@ public class SkiaCheckBox : SkiaView protected override void OnEnabledChanged() { base.OnEnabledChanged(); - SkiaVisualStateManager.GoToState(this, IsEnabled ? SkiaVisualStateManager.CommonStates.Normal : SkiaVisualStateManager.CommonStates.Disabled); + SkiaVisualStateManager.GoToState(this, IsEnabled ? "Normal" : "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; + return new SKSize(BoxSize + 8f, BoxSize + 8f); } } diff --git a/Views/SkiaCollectionView.cs b/Views/SkiaCollectionView.cs index 9860d79..74ab1eb 100644 --- a/Views/SkiaCollectionView.cs +++ b/Views/SkiaCollectionView.cs @@ -1,9 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Maui.Controls; using SkiaSharp; -using System.Collections; -using Microsoft.Maui.Graphics; namespace Microsoft.Maui.Platform; @@ -33,203 +35,110 @@ public class SkiaCollectionView : SkiaItemsView { #region BindableProperties - /// - /// 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 SelectionModeProperty = BindableProperty.Create( + nameof(SelectionMode), + typeof(SkiaSelectionMode), + typeof(SkiaCollectionView), + SkiaSelectionMode.Single, + BindingMode.TwoWay, + propertyChanged: (b, o, n) => ((SkiaCollectionView)b).OnSelectionModeChanged()); - /// - /// 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 SelectedItemProperty = BindableProperty.Create( + nameof(SelectedItem), + typeof(object), + typeof(SkiaCollectionView), + null, + BindingMode.OneWay, + propertyChanged: (b, o, n) => ((SkiaCollectionView)b).OnSelectedItemChanged(n)); - /// - /// 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 OrientationProperty = BindableProperty.Create( + nameof(Orientation), + typeof(ItemsLayoutOrientation), + typeof(SkiaCollectionView), + ItemsLayoutOrientation.Vertical, + BindingMode.TwoWay, + propertyChanged: (b, o, n) => ((SkiaCollectionView)b).Invalidate()); - /// - /// 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 SpanCountProperty = BindableProperty.Create( + nameof(SpanCount), + typeof(int), + typeof(SkiaCollectionView), + 1, + BindingMode.TwoWay, + propertyChanged: (b, o, n) => ((SkiaCollectionView)b).Invalidate(), + coerceValue: (b, v) => Math.Max(1, (int)v)); - /// - /// 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 GridItemWidthProperty = BindableProperty.Create( + nameof(GridItemWidth), + typeof(float), + typeof(SkiaCollectionView), + 100f, + BindingMode.TwoWay, + propertyChanged: (b, o, n) => ((SkiaCollectionView)b).Invalidate()); - /// - /// 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 HeaderProperty = BindableProperty.Create( + nameof(Header), + typeof(object), + typeof(SkiaCollectionView), + null, + BindingMode.TwoWay, + propertyChanged: (b, o, n) => ((SkiaCollectionView)b).OnHeaderChanged(n)); - /// - /// 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 FooterProperty = BindableProperty.Create( + nameof(Footer), + typeof(object), + typeof(SkiaCollectionView), + null, + BindingMode.TwoWay, + propertyChanged: (b, o, n) => ((SkiaCollectionView)b).OnFooterChanged(n)); - /// - /// 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 HeaderHeightProperty = BindableProperty.Create( + nameof(HeaderHeight), + typeof(float), + typeof(SkiaCollectionView), + 0f, + BindingMode.TwoWay, + propertyChanged: (b, o, n) => ((SkiaCollectionView)b).Invalidate()); - /// - /// 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 FooterHeightProperty = BindableProperty.Create( + nameof(FooterHeight), + typeof(float), + typeof(SkiaCollectionView), + 0f, + BindingMode.TwoWay, + propertyChanged: (b, o, n) => ((SkiaCollectionView)b).Invalidate()); - /// - /// 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 SelectionColorProperty = BindableProperty.Create( + nameof(SelectionColor), + typeof(SKColor), + typeof(SkiaCollectionView), + new SKColor(33, 150, 243, 89), + BindingMode.TwoWay, + propertyChanged: (b, o, n) => ((SkiaCollectionView)b).Invalidate()); - /// - /// 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()); + public static readonly BindableProperty HeaderBackgroundColorProperty = BindableProperty.Create( + nameof(HeaderBackgroundColor), + typeof(SKColor), + typeof(SkiaCollectionView), + new SKColor(245, 245, 245), + BindingMode.TwoWay, + propertyChanged: (b, o, n) => ((SkiaCollectionView)b).Invalidate()); - /// - /// 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()); + public static readonly BindableProperty FooterBackgroundColorProperty = BindableProperty.Create( + nameof(FooterBackgroundColor), + typeof(SKColor), + typeof(SkiaCollectionView), + new SKColor(245, 245, 245), + BindingMode.TwoWay, + propertyChanged: (b, o, n) => ((SkiaCollectionView)b).Invalidate()); #endregion - private List _selectedItems = new(); + private List _selectedItems = new List(); private int _selectedIndex = -1; - - // Track if heights changed during draw (requires redraw for correct positioning) + private bool _isSelectingItem; private bool _heightsChangedDuringDraw; - // Uses parent's _itemViewCache for virtualization - - protected override void RefreshItems() - { - // Clear selection when items change to avoid stale references - _selectedItems.Clear(); - SetValue(SelectedItemProperty, null); - _selectedIndex = -1; - - base.RefreshItems(); - } - - 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(); - } - - private void OnSelectedItemChanged(object? newValue) - { - if (SelectionMode == SkiaSelectionMode.None) return; - - ClearSelection(); - if (newValue != null) - { - SelectItem(newValue); - } - } - - private void OnHeaderChanged(object? newValue) - { - HeaderHeight = newValue != null ? 44 : 0; - Invalidate(); - } - - private void OnFooterChanged(object? newValue) - { - FooterHeight = newValue != null ? 44 : 0; - Invalidate(); - } - public SkiaSelectionMode SelectionMode { get => (SkiaSelectionMode)GetValue(SelectionModeProperty); @@ -249,12 +158,13 @@ public class SkiaCollectionView : SkiaItemsView get => _selectedIndex; set { - if (SelectionMode == SkiaSelectionMode.None) return; - - var item = GetItemAt(value); - if (item != null) + if (SelectionMode != SkiaSelectionMode.None) { - SelectedItem = item; + var item = GetItemAt(value); + if (item != null) + { + SelectedItem = item; + } } } } @@ -321,11 +231,68 @@ public class SkiaCollectionView : SkiaItemsView public event EventHandler? SelectionChanged; + protected override void RefreshItems() + { + _selectedItems.Clear(); + SetValue(SelectedItemProperty, null); + _selectedIndex = -1; + base.RefreshItems(); + } + + private void OnSelectionModeChanged() + { + switch (SelectionMode) + { + case SkiaSelectionMode.None: + ClearSelection(); + break; + case SkiaSelectionMode.Single: + if (_selectedItems.Count > 1) + { + var first = _selectedItems.FirstOrDefault(); + ClearSelection(); + if (first != null) + { + SelectItem(first); + } + } + break; + } + Invalidate(); + } + + private void OnSelectedItemChanged(object? newValue) + { + if (SelectionMode != SkiaSelectionMode.None && !_isSelectingItem) + { + ClearSelection(); + if (newValue != null) + { + SelectItem(newValue); + } + } + } + + private void OnHeaderChanged(object? newValue) + { + HeaderHeight = newValue != null ? 44 : 0; + Invalidate(); + } + + private void OnFooterChanged(object? newValue) + { + FooterHeight = newValue != null ? 44 : 0; + Invalidate(); + } + private void SelectItem(object item) { - if (SelectionMode == SkiaSelectionMode.None) return; + if (SelectionMode == SkiaSelectionMode.None) + { + return; + } - var oldSelectedItems = _selectedItems.ToList(); + var previousSelection = _selectedItems.ToList(); if (SelectionMode == SkiaSelectionMode.Single) { @@ -333,7 +300,6 @@ public class SkiaCollectionView : SkiaItemsView _selectedItems.Add(item); SetValue(SelectedItemProperty, item); - // Find index for (int i = 0; i < ItemCount; i++) { if (GetItemAt(i) == item) @@ -343,7 +309,7 @@ public class SkiaCollectionView : SkiaItemsView } } } - else // Multiple + else { if (_selectedItems.Contains(item)) { @@ -358,11 +324,10 @@ public class SkiaCollectionView : SkiaItemsView _selectedItems.Add(item); SetValue(SelectedItemProperty, item); } - _selectedIndex = SelectedItem != null ? GetIndexOf(SelectedItem) : -1; } - SelectionChanged?.Invoke(this, new CollectionSelectionChangedEventArgs(oldSelectedItems, _selectedItems.ToList())); + SelectionChanged?.Invoke(this, new CollectionSelectionChangedEventArgs(previousSelection, _selectedItems.ToList())); Invalidate(); } @@ -371,51 +336,62 @@ public class SkiaCollectionView : SkiaItemsView for (int i = 0; i < ItemCount; i++) { if (GetItemAt(i) == item) + { return i; + } } return -1; } private void ClearSelection() { - var oldItems = _selectedItems.ToList(); + var previousItems = _selectedItems.ToList(); _selectedItems.Clear(); SetValue(SelectedItemProperty, null); _selectedIndex = -1; - if (oldItems.Count > 0) + if (previousItems.Count > 0) { - SelectionChanged?.Invoke(this, new CollectionSelectionChangedEventArgs(oldItems, new List())); + SelectionChanged?.Invoke(this, new CollectionSelectionChangedEventArgs(previousItems, new List())); } } protected override void OnItemTapped(int index, object item) { - if (SelectionMode != SkiaSelectionMode.None) + if (_isSelectingItem) { - SelectItem(item); + return; } - base.OnItemTapped(index, item); + _isSelectingItem = true; + try + { + if (SelectionMode != SkiaSelectionMode.None) + { + SelectItem(item); + } + base.OnItemTapped(index, item); + } + finally + { + _isSelectingItem = false; + } } 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.Color = new SKColor(224, 224, 224); paint.Style = SKPaintStyle.Stroke; - paint.StrokeWidth = 1; + paint.StrokeWidth = 1f; 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); @@ -430,64 +406,56 @@ public class SkiaCollectionView : SkiaItemsView { 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) + if (float.IsNaN(rawHeight) || float.IsInfinity(rawHeight) || rawHeight > 10000f) { rawHeight = ItemHeight; } - // Ensure minimum height + var measuredHeight = Math.Max(rawHeight, ItemHeight); - if (!_itemHeights.TryGetValue(index, out var cachedHeight) || Math.Abs(cachedHeight - measuredHeight) > 1) + if (!_itemHeights.TryGetValue(index, out var cachedHeight) || Math.Abs(cachedHeight - measuredHeight) > 1f) { _itemHeights[index] = measuredHeight; - _heightsChangedDuringDraw = true; // Flag for redraw with correct positions + _heightsChangedDuringDraw = true; } - // 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); + canvas.DrawRoundRect(actualBounds, 12f, 12f, 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)); + DrawCheckmark(canvas, new SKRect(actualBounds.Right - 32f, actualBounds.MidY - 8f, actualBounds.Right - 16f, actualBounds.MidY + 8f)); } + return; } catch (Exception ex) { - Console.WriteLine($"[SkiaCollectionView.DrawItem] EXCEPTION: {ex.Message}\n{ex.StackTrace}"); + Console.WriteLine("[SkiaCollectionView.DrawItem] EXCEPTION: " + ex.Message + "\n" + ex.StackTrace); + return; } - return; } } - // Use custom renderer if provided - if (ItemRenderer != null) + if (ItemRenderer != null && ItemRenderer(item, index, bounds, canvas, paint)) { - if (ItemRenderer(item, index, bounds, canvas, paint)) - return; + 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 font = new SKFont(SKTypeface.Default, 14f, 1f, 0f); using var textPaint = new SKPaint(font) { Color = SKColors.Black, @@ -498,14 +466,13 @@ public class SkiaCollectionView : SkiaItemsView var textBounds = new SKRect(); textPaint.MeasureText(text, ref textBounds); - var x = bounds.Left + 16; + var x = bounds.Left + 16f; 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)); + DrawCheckmark(canvas, new SKRect(bounds.Right - 32f, bounds.MidY - 8f, bounds.Right - 16f, bounds.MidY + 8f)); } } @@ -513,27 +480,25 @@ public class SkiaCollectionView : SkiaItemsView { using var paint = new SKPaint { - Color = new SKColor(0x21, 0x96, 0xF3), + Color = new SKColor(33, 150, 243), Style = SKPaintStyle.Stroke, - StrokeWidth = 2, + StrokeWidth = 2f, 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); + path.LineTo(bounds.MidX - 2f, bounds.Bottom - 2f); + path.LineTo(bounds.Right, bounds.Top + 2f); 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 @@ -544,35 +509,26 @@ public class SkiaCollectionView : SkiaItemsView canvas.DrawRect(bounds, bgPaint); } - // Draw header if present - if (Header != null && HeaderHeight > 0) + if (Header != null && HeaderHeight > 0f) { 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) + if (Footer != null && FooterHeight > 0f) { 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); + 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); @@ -582,8 +538,6 @@ public class SkiaCollectionView : SkiaItemsView 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; @@ -593,17 +547,18 @@ public class SkiaCollectionView : SkiaItemsView private void DrawListItems(SKCanvas canvas, SKRect bounds) { - // Standard list drawing with variable item heights canvas.Save(); - canvas.ClipRect(bounds); + canvas.ClipRect(bounds, SKClipOperation.Intersect, false); - using var paint = new SKPaint { IsAntialias = true }; + using var paint = new SKPaint + { + IsAntialias = true + }; var scrollOffset = GetScrollOffset(); - // Find first visible item by walking through items int firstVisible = 0; - float cumulativeOffset = 0; + float cumulativeOffset = 0f; for (int i = 0; i < ItemCount; i++) { var itemH = GetItemHeight(i); @@ -615,16 +570,16 @@ public class SkiaCollectionView : SkiaItemsView 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); + var itemRect = new SKRect(bounds.Left, currentY, bounds.Right - 8f, currentY + itemH); - // Stop if we've passed the visible area if (itemRect.Top > bounds.Bottom) + { break; + } if (itemRect.Bottom >= bounds.Top) { @@ -640,7 +595,6 @@ public class SkiaCollectionView : SkiaItemsView canvas.Restore(); - // Draw scrollbar var totalHeight = TotalContentHeight; if (totalHeight > bounds.Height) { @@ -651,46 +605,51 @@ public class SkiaCollectionView : SkiaItemsView private void DrawGridItems(SKCanvas canvas, SKRect bounds) { canvas.Save(); - canvas.ClipRect(bounds); + canvas.ClipRect(bounds, SKClipOperation.Intersect, false); - using var paint = new SKPaint { IsAntialias = true }; + using var paint = new SKPaint + { + IsAntialias = true + }; - var cellWidth = (bounds.Width - 8) / SpanCount; // -8 for scrollbar + var cellWidth = (bounds.Width - 8f) / SpanCount; 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); + 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; + var rowY = bounds.Top + row * (cellHeight + ItemSpacing) - scrollOffset; for (int col = 0; col < SpanCount; col++) { var index = row * SpanCount + col; - if (index >= ItemCount) break; + if (index >= ItemCount) + { + break; + } var cellX = bounds.Left + col * cellWidth; - var cellRect = new SKRect(cellX + 2, rowY, cellX + cellWidth - 2, rowY + cellHeight); + var cellRect = new SKRect(cellX + 2f, rowY, cellX + cellWidth - 2f, 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), + Color = _selectedItems.Contains(item) ? SelectionColor : new SKColor(250, 250, 250), Style = SKPaintStyle.Fill }; - canvas.DrawRoundRect(new SKRoundRect(cellRect, 4), cellBgPaint); - + canvas.DrawRoundRect(new SKRoundRect(cellRect, 4f), cellBgPaint); DrawItem(canvas, item, index, cellRect, paint); } } @@ -698,7 +657,6 @@ public class SkiaCollectionView : SkiaItemsView canvas.Restore(); - // Draw scrollbar if (totalHeight > bounds.Height) { DrawScrollBarInternal(canvas, bounds, scrollOffset, totalHeight); @@ -710,7 +668,6 @@ public class SkiaCollectionView : SkiaItemsView var scrollBarWidth = 6f; var scrollBarMargin = 2f; - // Draw scrollbar track (subtle) var trackRect = new SKRect( bounds.Right - scrollBarWidth - scrollBarMargin, bounds.Top + scrollBarMargin, @@ -722,23 +679,17 @@ public class SkiaCollectionView : SkiaItemsView Color = new SKColor(0, 0, 0, 20), Style = SKPaintStyle.Fill }; - canvas.DrawRoundRect(new SKRoundRect(trackRect, 3), trackPaint); + canvas.DrawRoundRect(new SKRoundRect(trackRect, 3f), trackPaint); - // Calculate thumb position and size - var maxOffset = Math.Max(0, totalHeight - bounds.Height); + var maxOffset = Math.Max(0f, 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 thumbHeight = Math.Max(30f, availableTrackHeight * viewportRatio); + var scrollRatio = maxOffset > 0f ? scrollOffset / maxOffset : 0f; var thumbY = trackRect.Top + (availableTrackHeight - thumbHeight) * scrollRatio; - var thumbRect = new SKRect( - trackRect.Left, - thumbY, - trackRect.Right, - thumbY + thumbHeight); + 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), @@ -746,13 +697,11 @@ public class SkiaCollectionView : SkiaItemsView IsAntialias = true }; - canvas.DrawRoundRect(new SKRoundRect(thumbRect, 3), thumbPaint); + canvas.DrawRoundRect(new SKRoundRect(thumbRect, 3f), 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; } @@ -765,11 +714,10 @@ public class SkiaCollectionView : SkiaItemsView }; canvas.DrawRect(bounds, bgPaint); - // Draw header text - var text = Header.ToString() ?? ""; + var text = Header?.ToString() ?? ""; if (!string.IsNullOrEmpty(text)) { - using var font = new SKFont(SKTypeface.Default, 16); + using var font = new SKFont(SKTypeface.Default, 16f, 1f, 0f); using var textPaint = new SKPaint(font) { Color = SKColors.Black, @@ -779,17 +727,16 @@ public class SkiaCollectionView : SkiaItemsView var textBounds = new SKRect(); textPaint.MeasureText(text, ref textBounds); - var x = bounds.Left + 16; + var x = bounds.Left + 16f; 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), + Color = new SKColor(224, 224, 224), Style = SKPaintStyle.Stroke, - StrokeWidth = 1 + StrokeWidth = 1f }; canvas.DrawLine(bounds.Left, bounds.Bottom, bounds.Right, bounds.Bottom, sepPaint); } @@ -803,23 +750,21 @@ public class SkiaCollectionView : SkiaItemsView }; canvas.DrawRect(bounds, bgPaint); - // Draw separator using var sepPaint = new SKPaint { - Color = new SKColor(0xE0, 0xE0, 0xE0), + Color = new SKColor(224, 224, 224), Style = SKPaintStyle.Stroke, - StrokeWidth = 1 + StrokeWidth = 1f }; canvas.DrawLine(bounds.Left, bounds.Top, bounds.Right, bounds.Top, sepPaint); - // Draw footer text - var text = Footer.ToString() ?? ""; + var text = Footer?.ToString() ?? ""; if (!string.IsNullOrEmpty(text)) { - using var font = new SKFont(SKTypeface.Default, 14); + using var font = new SKFont(SKTypeface.Default, 14f, 1f, 0f); using var textPaint = new SKPaint(font) { - Color = new SKColor(0x80, 0x80, 0x80), + Color = new SKColor(128, 128, 128), IsAntialias = true }; diff --git a/Views/SkiaDatePicker.cs b/Views/SkiaDatePicker.cs index 163adaa..90c17fc 100644 --- a/Views/SkiaDatePicker.cs +++ b/Views/SkiaDatePicker.cs @@ -1,8 +1,10 @@ // 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; +using Microsoft.Maui.Controls; using Microsoft.Maui.Platform.Linux; +using SkiaSharp; namespace Microsoft.Maui.Platform; @@ -14,59 +16,70 @@ public class SkiaDatePicker : SkiaView #region BindableProperties public static readonly BindableProperty DateProperty = - BindableProperty.Create(nameof(Date), typeof(DateTime), typeof(SkiaDatePicker), DateTime.Today, BindingMode.TwoWay, + BindableProperty.Create(nameof(Date), typeof(DateTime), typeof(SkiaDatePicker), DateTime.Today, BindingMode.OneWay, propertyChanged: (b, o, n) => ((SkiaDatePicker)b).OnDatePropertyChanged()); public static readonly BindableProperty MinimumDateProperty = - BindableProperty.Create(nameof(MinimumDate), typeof(DateTime), typeof(SkiaDatePicker), new DateTime(1900, 1, 1), + BindableProperty.Create(nameof(MinimumDate), typeof(DateTime), typeof(SkiaDatePicker), new DateTime(1900, 1, 1), BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaDatePicker)b).Invalidate()); public static readonly BindableProperty MaximumDateProperty = - BindableProperty.Create(nameof(MaximumDate), typeof(DateTime), typeof(SkiaDatePicker), new DateTime(2100, 12, 31), + BindableProperty.Create(nameof(MaximumDate), typeof(DateTime), typeof(SkiaDatePicker), new DateTime(2100, 12, 31), BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaDatePicker)b).Invalidate()); public static readonly BindableProperty FormatProperty = - BindableProperty.Create(nameof(Format), typeof(string), typeof(SkiaDatePicker), "d", + BindableProperty.Create(nameof(Format), typeof(string), typeof(SkiaDatePicker), "d", BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaDatePicker)b).Invalidate()); public static readonly BindableProperty TextColorProperty = - BindableProperty.Create(nameof(TextColor), typeof(SKColor), typeof(SkiaDatePicker), SKColors.Black, + BindableProperty.Create(nameof(TextColor), typeof(SKColor), typeof(SkiaDatePicker), SKColors.Black, BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaDatePicker)b).Invalidate()); public static readonly BindableProperty BorderColorProperty = - BindableProperty.Create(nameof(BorderColor), typeof(SKColor), typeof(SkiaDatePicker), new SKColor(0xBD, 0xBD, 0xBD), + BindableProperty.Create(nameof(BorderColor), typeof(SKColor), typeof(SkiaDatePicker), new SKColor(189, 189, 189), BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaDatePicker)b).Invalidate()); public static readonly BindableProperty CalendarBackgroundColorProperty = - BindableProperty.Create(nameof(CalendarBackgroundColor), typeof(SKColor), typeof(SkiaDatePicker), SKColors.White, + BindableProperty.Create(nameof(CalendarBackgroundColor), typeof(SKColor), typeof(SkiaDatePicker), SKColors.White, BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaDatePicker)b).Invalidate()); public static readonly BindableProperty SelectedDayColorProperty = - BindableProperty.Create(nameof(SelectedDayColor), typeof(SKColor), typeof(SkiaDatePicker), new SKColor(0x21, 0x96, 0xF3), + BindableProperty.Create(nameof(SelectedDayColor), typeof(SKColor), typeof(SkiaDatePicker), new SKColor(33, 150, 243), BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaDatePicker)b).Invalidate()); public static readonly BindableProperty TodayColorProperty = - BindableProperty.Create(nameof(TodayColor), typeof(SKColor), typeof(SkiaDatePicker), new SKColor(0x21, 0x96, 0xF3, 0x40), + BindableProperty.Create(nameof(TodayColor), typeof(SKColor), typeof(SkiaDatePicker), new SKColor(33, 150, 243, 64), BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaDatePicker)b).Invalidate()); public static readonly BindableProperty HeaderColorProperty = - BindableProperty.Create(nameof(HeaderColor), typeof(SKColor), typeof(SkiaDatePicker), new SKColor(0x21, 0x96, 0xF3), + BindableProperty.Create(nameof(HeaderColor), typeof(SKColor), typeof(SkiaDatePicker), new SKColor(33, 150, 243), BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaDatePicker)b).Invalidate()); public static readonly BindableProperty DisabledDayColorProperty = - BindableProperty.Create(nameof(DisabledDayColor), typeof(SKColor), typeof(SkiaDatePicker), new SKColor(0xBD, 0xBD, 0xBD), + BindableProperty.Create(nameof(DisabledDayColor), typeof(SKColor), typeof(SkiaDatePicker), new SKColor(189, 189, 189), BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaDatePicker)b).Invalidate()); public static readonly BindableProperty FontSizeProperty = - BindableProperty.Create(nameof(FontSize), typeof(float), typeof(SkiaDatePicker), 14f, + BindableProperty.Create(nameof(FontSize), typeof(float), typeof(SkiaDatePicker), 14f, BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaDatePicker)b).InvalidateMeasure()); public static readonly BindableProperty CornerRadiusProperty = - BindableProperty.Create(nameof(CornerRadius), typeof(float), typeof(SkiaDatePicker), 4f, + BindableProperty.Create(nameof(CornerRadius), typeof(float), typeof(SkiaDatePicker), 4f, BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaDatePicker)b).Invalidate()); #endregion + #region Fields + + private DateTime _displayMonth; + private bool _isOpen; + + private const float CalendarWidth = 280f; + private const float CalendarHeight = 320f; + private const float HeaderHeight = 48f; + + #endregion + #region Properties public DateTime Date @@ -156,9 +169,9 @@ public class SkiaDatePicker : SkiaView { _isOpen = value; if (_isOpen) - RegisterPopupOverlay(this, DrawCalendarOverlay); + SkiaView.RegisterPopupOverlay(this, DrawCalendarOverlay); else - UnregisterPopupOverlay(this); + SkiaView.UnregisterPopupOverlay(this); Invalidate(); } } @@ -166,44 +179,13 @@ public class SkiaDatePicker : SkiaView #endregion - private DateTime _displayMonth; - private bool _isOpen; - - private const float CalendarWidth = 280; - private const float CalendarHeight = 320; - private const float HeaderHeight = 48; + #region Events public event EventHandler? DateSelected; - /// - /// 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; + #endregion - // Calculate default position (below the picker) - var calendarLeft = pickerBounds.Left; - var calendarTop = pickerBounds.Bottom + 4; - - // Edge detection: adjust horizontal position if popup would go off-screen - if (calendarLeft + CalendarWidth > windowWidth) - { - calendarLeft = windowWidth - CalendarWidth - 4; - } - if (calendarLeft < 0) calendarLeft = 4; - - // 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; - - return new SKRect(calendarLeft, calendarTop, calendarLeft + CalendarWidth, calendarTop + CalendarHeight); - } + #region Constructor public SkiaDatePicker() { @@ -211,6 +193,38 @@ public class SkiaDatePicker : SkiaView _displayMonth = new DateTime(DateTime.Today.Year, DateTime.Today.Month, 1); } + #endregion + + #region Private Methods + + private SKRect GetCalendarRect(SKRect pickerBounds) + { + int windowWidth = LinuxApplication.Current?.MainWindow?.Width ?? 800; + int windowHeight = LinuxApplication.Current?.MainWindow?.Height ?? 600; + + float calendarLeft = pickerBounds.Left; + float calendarTop = pickerBounds.Bottom + 4f; + + if (calendarLeft + CalendarWidth > windowWidth) + { + calendarLeft = windowWidth - CalendarWidth - 4f; + } + if (calendarLeft < 0f) + { + calendarLeft = 4f; + } + if (calendarTop + CalendarHeight > windowHeight) + { + calendarTop = pickerBounds.Top - CalendarHeight - 4f; + } + if (calendarTop < 0f) + { + calendarTop = 4f; + } + + return new SKRect(calendarLeft, calendarTop, calendarLeft + CalendarWidth, calendarTop + CalendarHeight); + } + private void OnDatePropertyChanged() { _displayMonth = new DateTime(Date.Year, Date.Month, 1); @@ -220,28 +234,26 @@ public class SkiaDatePicker : SkiaView private DateTime ClampDate(DateTime date) { - if (date < MinimumDate) return MinimumDate; - if (date > MaximumDate) return MaximumDate; + if (date < MinimumDate) + return MinimumDate; + if (date > MaximumDate) + return MaximumDate; return date; } private void DrawCalendarOverlay(SKCanvas canvas) { - if (!_isOpen) return; - // Use ScreenBounds for popup drawing (accounts for scroll offset) - DrawCalendar(canvas, ScreenBounds); - } - - protected override void OnDraw(SKCanvas canvas, SKRect bounds) - { - DrawPickerButton(canvas, bounds); + if (_isOpen) + { + DrawCalendar(canvas, ScreenBounds); + } } private void DrawPickerButton(SKCanvas canvas, SKRect bounds) { using var bgPaint = new SKPaint { - Color = IsEnabled ? BackgroundColor : new SKColor(0xF5, 0xF5, 0xF5), + Color = IsEnabled ? BackgroundColor : new SKColor(245, 245, 245), Style = SKPaintStyle.Fill, IsAntialias = true }; @@ -256,19 +268,19 @@ public class SkiaDatePicker : SkiaView }; canvas.DrawRoundRect(new SKRoundRect(bounds, CornerRadius), borderPaint); - using var font = new SKFont(SKTypeface.Default, FontSize); + using var font = new SKFont(SKTypeface.Default, FontSize, 1f, 0f); using var textPaint = new SKPaint(font) { Color = IsEnabled ? TextColor : TextColor.WithAlpha(128), IsAntialias = true }; - var dateText = Date.ToString(Format); - var textBounds = new SKRect(); + string dateText = Date.ToString(Format); + SKRect textBounds = default; textPaint.MeasureText(dateText, ref textBounds); - canvas.DrawText(dateText, bounds.Left + 12, bounds.MidY - textBounds.MidY, textPaint); + canvas.DrawText(dateText, bounds.Left + 12f, bounds.MidY - textBounds.MidY, textPaint); - DrawCalendarIcon(canvas, new SKRect(bounds.Right - 36, bounds.MidY - 10, bounds.Right - 12, bounds.MidY + 10)); + DrawCalendarIcon(canvas, new SKRect(bounds.Right - 36f, bounds.MidY - 10f, bounds.Right - 12f, bounds.MidY + 10f)); } private void DrawCalendarIcon(SKCanvas canvas, SKRect bounds) @@ -281,162 +293,230 @@ public class SkiaDatePicker : SkiaView 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); + SKRect calRect = new SKRect(bounds.Left, bounds.Top + 3f, bounds.Right, bounds.Bottom); + canvas.DrawRoundRect(new SKRoundRect(calRect, 2f), paint); + canvas.DrawLine(bounds.Left + 5f, bounds.Top, bounds.Left + 5f, bounds.Top + 5f, paint); + canvas.DrawLine(bounds.Right - 5f, bounds.Top, bounds.Right - 5f, bounds.Top + 5f, paint); + canvas.DrawLine(bounds.Left, bounds.Top + 8f, bounds.Right, bounds.Top + 8f, 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); + for (int i = 0; i < 2; i++) + { + for (int j = 0; j < 3; j++) + { + canvas.DrawCircle(bounds.Left + 4f + j * 6, bounds.Top + 12f + i * 4, 1f, paint); + } + } } private void DrawCalendar(SKCanvas canvas, SKRect bounds) { - var calendarRect = GetCalendarRect(bounds); + SKRect calendarRect = GetCalendarRect(bounds); using var shadowPaint = new SKPaint { Color = new SKColor(0, 0, 0, 40), - MaskFilter = SKMaskFilter.CreateBlur(SKBlurStyle.Normal, 4), + MaskFilter = SKMaskFilter.CreateBlur(SKBlurStyle.Normal, 4f), Style = SKPaintStyle.Fill }; - canvas.DrawRoundRect(new SKRoundRect(new SKRect(calendarRect.Left + 2, calendarRect.Top + 2, calendarRect.Right + 2, calendarRect.Bottom + 2), CornerRadius), shadowPaint); + canvas.DrawRoundRect(new SKRoundRect(new SKRect(calendarRect.Left + 2f, calendarRect.Top + 2f, calendarRect.Right + 2f, calendarRect.Bottom + 2f), CornerRadius), shadowPaint); - using var bgPaint = new SKPaint { Color = CalendarBackgroundColor, Style = SKPaintStyle.Fill, IsAntialias = true }; + 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 }; + using var borderPaint = new SKPaint + { + Color = BorderColor, + Style = SKPaintStyle.Stroke, + StrokeWidth = 1f, + 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)); + DrawCalendarHeader(canvas, new SKRect(calendarRect.Left, calendarRect.Top, calendarRect.Right, calendarRect.Top + 48f)); + DrawWeekdayHeaders(canvas, new SKRect(calendarRect.Left, calendarRect.Top + 48f, calendarRect.Right, calendarRect.Top + 48f + 30f)); + DrawDays(canvas, new SKRect(calendarRect.Left, calendarRect.Top + 48f + 30f, calendarRect.Right, calendarRect.Bottom)); } private void DrawCalendarHeader(SKCanvas canvas, SKRect bounds) { - using var headerPaint = new SKPaint { Color = HeaderColor, Style = SKPaintStyle.Fill }; + 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.ClipRoundRect(new SKRoundRect(new SKRect(bounds.Left, bounds.Top, bounds.Right, bounds.Top + CornerRadius * 2f), CornerRadius), SKClipOperation.Intersect, false); 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(); + using var font = new SKFont(SKTypeface.Default, 16f, 1f, 0f); + using var textPaint = new SKPaint(font) + { + Color = SKColors.White, + IsAntialias = true + }; + + string monthYear = _displayMonth.ToString("MMMM yyyy"); + SKRect textBounds = default; 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 arrowPaint = new SKPaint + { + Color = SKColors.White, + Style = SKPaintStyle.Stroke, + StrokeWidth = 2f, + 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); + leftPath.MoveTo(bounds.Left + 26f, bounds.MidY - 6f); + leftPath.LineTo(bounds.Left + 20f, bounds.MidY); + leftPath.LineTo(bounds.Left + 26f, bounds.MidY + 6f); 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); + rightPath.MoveTo(bounds.Right - 26f, bounds.MidY - 6f); + rightPath.LineTo(bounds.Right - 20f, bounds.MidY); + rightPath.LineTo(bounds.Right - 26f, bounds.MidY + 6f); 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 }; + string[] dayNames = new string[] { "Su", "Mo", "Tu", "We", "Th", "Fr", "Sa" }; + float cellWidth = bounds.Width / 7f; + + using var font = new SKFont(SKTypeface.Default, 12f, 1f, 0f); + using var paint = new SKPaint(font) + { + Color = new SKColor(128, 128, 128), + IsAntialias = true + }; + for (int i = 0; i < 7; i++) { - var textBounds = new SKRect(); + SKRect textBounds = default; paint.MeasureText(dayNames[i], ref textBounds); - canvas.DrawText(dayNames[i], bounds.Left + i * cellWidth + cellWidth / 2 - textBounds.MidX, bounds.MidY - textBounds.MidY, paint); + canvas.DrawText(dayNames[i], bounds.Left + i * cellWidth + cellWidth / 2f - 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; + DateTime firstDay = new DateTime(_displayMonth.Year, _displayMonth.Month, 1); + int daysInMonth = DateTime.DaysInMonth(_displayMonth.Year, _displayMonth.Month); + int startDayOfWeek = (int)firstDay.DayOfWeek; + float cellWidth = bounds.Width / 7f; + float cellHeight = (bounds.Height - 10f) / 6f; + + using var font = new SKFont(SKTypeface.Default, 14f, 1f, 0f); + using var textPaint = new SKPaint(font) + { + IsAntialias = true + }; + using var bgPaint = new SKPaint + { + Style = SKPaintStyle.Fill, + IsAntialias = true + }; + + DateTime today = DateTime.Today; + SKRect cellRect = default; 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); + DateTime dayDate = new DateTime(_displayMonth.Year, _displayMonth.Month, day); + int cellIndex = startDayOfWeek + day - 1; + int row = cellIndex / 7; + int col = cellIndex % 7; - var isSelected = dayDate.Date == Date.Date; - var isToday = dayDate.Date == today; - var isDisabled = dayDate < MinimumDate || dayDate > MaximumDate; + cellRect = new SKRect( + bounds.Left + col * cellWidth + 2f, + bounds.Top + row * cellHeight + 2f, + bounds.Left + (col + 1) * cellWidth - 2f, + bounds.Top + (row + 1) * cellHeight - 2f); + + bool isSelected = dayDate.Date == Date.Date; + bool isToday = dayDate.Date == today; + bool 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); + canvas.DrawCircle(cellRect.MidX, cellRect.MidY, Math.Min(cellRect.Width, cellRect.Height) / 2f - 2f, bgPaint); } else if (isToday) { bgPaint.Color = TodayColor; - canvas.DrawCircle(cellRect.MidX, cellRect.MidY, Math.Min(cellRect.Width, cellRect.Height) / 2 - 2, bgPaint); + canvas.DrawCircle(cellRect.MidX, cellRect.MidY, Math.Min(cellRect.Width, cellRect.Height) / 2f - 2f, 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); + textPaint.Color = isSelected ? SKColors.White : (isDisabled ? DisabledDayColor : TextColor); + string dayText = day.ToString(); + SKRect dayTextBounds = default; + textPaint.MeasureText(dayText, ref dayTextBounds); + canvas.DrawText(dayText, cellRect.MidX - dayTextBounds.MidX, cellRect.MidY - dayTextBounds.MidY, textPaint); } } + #endregion + + #region Overrides + + protected override void OnDraw(SKCanvas canvas, SKRect bounds) + { + DrawPickerButton(canvas, bounds); + } + public override void OnPointerPressed(PointerEventArgs e) { - if (!IsEnabled) return; + if (!IsEnabled) + return; if (IsOpen) { - // Use ScreenBounds for popup coordinate calculations (accounts for scroll offset) - var screenBounds = ScreenBounds; - var calendarRect = GetCalendarRect(screenBounds); + SKRect screenBounds = ScreenBounds; + SKRect 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); + SKRect headerRect = new SKRect(calendarRect.Left, calendarRect.Top, calendarRect.Right, calendarRect.Top + 48f); 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; } + if (e.X < calendarRect.Left + 40f) + { + _displayMonth = _displayMonth.AddMonths(-1); + Invalidate(); + } + else if (e.X > calendarRect.Right - 40f) + { + _displayMonth = _displayMonth.AddMonths(1); + Invalidate(); + } 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); + float daysTop = calendarRect.Top + 48f + 30f; + SKRect 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); + float cellWidth = 40f; + float cellHeight = 38.666668f; + int col = (int)((e.X - calendarRect.Left) / cellWidth); + int dayIndex = (int)((e.Y - daysTop) / cellHeight) * 7 + col - (int)new DateTime(_displayMonth.Year, _displayMonth.Month, 1).DayOfWeek + 1; + int daysInMonth = DateTime.DaysInMonth(_displayMonth.Year, _displayMonth.Month); + if (dayIndex >= 1 && dayIndex <= daysInMonth) { - var selectedDate = new DateTime(_displayMonth.Year, _displayMonth.Month, dayIndex); + DateTime selectedDate = new DateTime(_displayMonth.Year, _displayMonth.Month, dayIndex); if (selectedDate >= MinimumDate && selectedDate <= MaximumDate) { Date = selectedDate; @@ -446,27 +526,53 @@ public class SkiaDatePicker : SkiaView 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; + else + { + IsOpen = true; + } Invalidate(); } public override void OnKeyDown(KeyEventArgs e) { - if (!IsEnabled) return; + 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; + 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(); } @@ -474,7 +580,6 @@ public class SkiaDatePicker : SkiaView public override void OnFocusLost() { base.OnFocusLost(); - // Close popup when focus is lost (clicking outside) if (IsOpen) { IsOpen = false; @@ -483,28 +588,23 @@ public class SkiaDatePicker : SkiaView protected override SKSize MeasureOverride(SKSize availableSize) { - return new SKSize(availableSize.Width < float.MaxValue ? Math.Min(availableSize.Width, 200) : 200, 40); + return new SKSize(availableSize.Width < float.MaxValue ? Math.Min(availableSize.Width, 200f) : 200f, 40f); } - /// - /// 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 + SKRect screenBounds = ScreenBounds; if (screenBounds.Contains(x, y)) + { return true; - - // When open, also include the calendar area (with edge detection) + } if (_isOpen) { - var calendarRect = GetCalendarRect(screenBounds); + SKRect calendarRect = GetCalendarRect(screenBounds); return calendarRect.Contains(x, y); } - return false; } + + #endregion } diff --git a/Views/SkiaImage.cs b/Views/SkiaImage.cs index b38fc5b..06fb158 100644 --- a/Views/SkiaImage.cs +++ b/Views/SkiaImage.cs @@ -1,19 +1,33 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; +using System.Collections.Generic; +using System.IO; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; using SkiaSharp; -using Microsoft.Maui.Graphics; +using Svg.Skia; namespace Microsoft.Maui.Platform; /// -/// Skia-rendered image control. +/// Skia-rendered image control with SVG support. /// public class SkiaImage : SkiaView { private SKBitmap? _bitmap; private SKImage? _image; private bool _isLoading; + private string? _currentFilePath; + private bool _isSvg; + private CancellationTokenSource? _loadCts; + private readonly object _loadLock = new object(); + private double _svgLoadedWidth; + private double _svgLoadedHeight; + private bool _pendingSvgReload; + private SKRect _lastArrangedBounds; public SKBitmap? Bitmap { @@ -28,14 +42,64 @@ public class SkiaImage : SkiaView } } - public Aspect Aspect { get; set; } = Aspect.AspectFit; + public Aspect Aspect { get; set; } + public bool IsOpaque { get; set; } + public bool IsLoading => _isLoading; + public bool IsAnimationPlaying { get; set; } + public new double WidthRequest + { + get => base.WidthRequest; + set + { + base.WidthRequest = value; + ScheduleSvgReloadIfNeeded(); + } + } + + public new double HeightRequest + { + get => base.HeightRequest; + set + { + base.HeightRequest = value; + ScheduleSvgReloadIfNeeded(); + } + } + public event EventHandler? ImageLoaded; public event EventHandler? ImageLoadingError; + 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(); + } + } + } + + 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); + } + } + protected override void OnDraw(SKCanvas canvas, SKRect bounds) { // Draw background if not opaque @@ -49,14 +113,16 @@ public class SkiaImage : SkiaView canvas.DrawRect(bounds, bgPaint); } - if (_image == null) return; + if (_image == null) + return; - var imageWidth = _image.Width; - var imageHeight = _image.Height; + int width = _image.Width; + int height = _image.Height; - if (imageWidth <= 0 || imageHeight <= 0) return; + if (width <= 0 || height <= 0) + return; - var destRect = CalculateDestRect(bounds, imageWidth, imageHeight); + SKRect destRect = CalculateDestRect(bounds, width, height); using var paint = new SKPaint { @@ -69,37 +135,37 @@ public class SkiaImage : SkiaView private SKRect CalculateDestRect(SKRect bounds, float imageWidth, float imageHeight) { - float destX, destY, destWidth, destHeight; - switch (Aspect) { case Aspect.Fill: - // Stretch to fill entire bounds return bounds; 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; + { + float scale = Math.Min(bounds.Width / imageWidth, bounds.Height / imageHeight); + float destWidth = imageWidth * scale; + float destHeight = imageHeight * scale; + float destX = bounds.Left + (bounds.Width - destWidth) / 2f; + float destY = bounds.Top + (bounds.Height - destHeight) / 2f; return new SKRect(destX, destY, destX + destWidth, destY + destHeight); + } 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; + { + float scale = Math.Max(bounds.Width / imageWidth, bounds.Height / imageHeight); + float destWidth = imageWidth * scale; + float destHeight = imageHeight * scale; + float destX = bounds.Left + (bounds.Width - destWidth) / 2f; + float destY = bounds.Top + (bounds.Height - destHeight) / 2f; return new SKRect(destX, destY, destX + destWidth, destY + destHeight); + } case Aspect.Center: - // Center without scaling - destX = bounds.Left + (bounds.Width - imageWidth) / 2; - destY = bounds.Top + (bounds.Height - imageHeight) / 2; + { + float destX = bounds.Left + (bounds.Width - imageWidth) / 2f; + float destY = bounds.Top + (bounds.Height - imageHeight) / 2f; return new SKRect(destX, destY, destX + imageWidth, destY + imageHeight); + } default: return bounds; @@ -110,18 +176,69 @@ public class SkiaImage : SkiaView { _isLoading = true; Invalidate(); + Console.WriteLine($"[SkiaImage] LoadFromFileAsync: {filePath}, WidthRequest={WidthRequest}, HeightRequest={HeightRequest}"); try { - await Task.Run(() => + List searchPaths = new List { - using var stream = File.OpenRead(filePath); - var bitmap = SKBitmap.Decode(stream); - if (bitmap != null) + filePath, + Path.Combine(AppContext.BaseDirectory, filePath), + Path.Combine(AppContext.BaseDirectory, "Resources", "Images", filePath), + Path.Combine(AppContext.BaseDirectory, "Resources", filePath) + }; + + // Also try SVG if looking for PNG + if (filePath.EndsWith(".png", StringComparison.OrdinalIgnoreCase)) + { + string svgPath = Path.ChangeExtension(filePath, ".svg"); + searchPaths.Add(svgPath); + searchPaths.Add(Path.Combine(AppContext.BaseDirectory, svgPath)); + searchPaths.Add(Path.Combine(AppContext.BaseDirectory, "Resources", "Images", svgPath)); + searchPaths.Add(Path.Combine(AppContext.BaseDirectory, "Resources", svgPath)); + } + + string? foundPath = null; + foreach (string path in searchPaths) + { + if (File.Exists(path)) { - Bitmap = bitmap; + foundPath = path; + Console.WriteLine("[SkiaImage] Found file at: " + path); + break; } - }); + } + + if (foundPath == null) + { + Console.WriteLine("[SkiaImage] File not found: " + filePath); + _isLoading = false; + _isSvg = false; + _currentFilePath = null; + ImageLoadingError?.Invoke(this, new ImageLoadingErrorEventArgs(new FileNotFoundException(filePath))); + return; + } + + _isSvg = foundPath.EndsWith(".svg", StringComparison.OrdinalIgnoreCase); + _currentFilePath = foundPath; + + if (!_isSvg) + { + await Task.Run(() => + { + using FileStream stream = File.OpenRead(foundPath); + SKBitmap? bitmap = SKBitmap.Decode(stream); + if (bitmap != null) + { + Bitmap = bitmap; + Console.WriteLine("[SkiaImage] Loaded image: " + foundPath); + } + }); + } + else + { + await LoadSvgAtSizeAsync(foundPath, WidthRequest, HeightRequest); + } _isLoading = false; ImageLoaded?.Invoke(this, EventArgs.Empty); @@ -135,6 +252,69 @@ public class SkiaImage : SkiaView 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(() => + { + if (cts.Token.IsCancellationRequested) + return; + + using var svg = new SKSvg(); + svg.Load(svgPath); + + if (svg.Picture != null && !cts.Token.IsCancellationRequested) + { + SKRect cullRect = svg.Picture.CullRect; + + float requestedWidth = (targetWidth > 0.0) + ? (float)targetWidth + : ((cullRect.Width <= 24f) ? 24f : cullRect.Width); + + float requestedHeight = (targetHeight > 0.0) + ? (float)targetHeight + : ((cullRect.Height <= 24f) ? 24f : cullRect.Height); + + float scale = Math.Min(requestedWidth / cullRect.Width, requestedHeight / cullRect.Height); + + int bitmapWidth = Math.Max(1, (int)(cullRect.Width * scale)); + int bitmapHeight = Math.Max(1, (int)(cullRect.Height * scale)); + + newBitmap = new SKBitmap(bitmapWidth, bitmapHeight, false); + + using var canvas = new SKCanvas(newBitmap); + canvas.Clear(SKColors.Transparent); + canvas.Scale(scale); + canvas.DrawPicture(svg.Picture, null); + + Console.WriteLine($"[SkiaImage] Loaded SVG: {svgPath} at {bitmapWidth}x{bitmapHeight} (requested {targetWidth}x{targetHeight})"); + } + }, cts.Token); + + if (!cts.Token.IsCancellationRequested && newBitmap != null) + { + _svgLoadedWidth = (targetWidth > 0.0) ? targetWidth : newBitmap.Width; + _svgLoadedHeight = (targetHeight > 0.0) ? targetHeight : newBitmap.Height; + Bitmap = newBitmap; + } + else + { + newBitmap?.Dispose(); + } + } + catch (OperationCanceledException) + { + // Cancellation is expected when reloading SVG at different sizes + } + } + public async Task LoadFromStreamAsync(Stream stream) { _isLoading = true; @@ -144,7 +324,7 @@ public class SkiaImage : SkiaView { await Task.Run(() => { - var bitmap = SKBitmap.Decode(stream); + SKBitmap? bitmap = SKBitmap.Decode(stream); if (bitmap != null) { Bitmap = bitmap; @@ -170,11 +350,9 @@ public class SkiaImage : SkiaView try { - using var httpClient = new HttpClient(); - var data = await httpClient.GetByteArrayAsync(uri); - - using var stream = new MemoryStream(data); - var bitmap = SKBitmap.Decode(stream); + using HttpClient httpClient = new HttpClient(); + using MemoryStream stream = new MemoryStream(await httpClient.GetByteArrayAsync(uri)); + SKBitmap? bitmap = SKBitmap.Decode(stream); if (bitmap != null) { Bitmap = bitmap; @@ -196,8 +374,8 @@ public class SkiaImage : SkiaView { try { - using var stream = new MemoryStream(data); - var bitmap = SKBitmap.Decode(stream); + using MemoryStream stream = new MemoryStream(data); + SKBitmap? bitmap = SKBitmap.Decode(stream); if (bitmap != null) { Bitmap = bitmap; @@ -217,6 +395,8 @@ public class SkiaImage : SkiaView { try { + _isSvg = false; + _currentFilePath = null; Bitmap = bitmap; _isLoading = false; ImageLoaded?.Invoke(this, EventArgs.Empty); @@ -229,28 +409,84 @@ public class SkiaImage : SkiaView Invalidate(); } + public override void Arrange(SKRect bounds) + { + base.Arrange(bounds); + + // If no explicit size requested and this is an SVG, check if we need to reload at larger size + if (!(base.WidthRequest > 0.0) || !(base.HeightRequest > 0.0)) + { + if (_isSvg && !string.IsNullOrEmpty(_currentFilePath) && !_isLoading) + { + float width = bounds.Width; + float height = bounds.Height; + + if ((width > _svgLoadedWidth * 1.1 || height > _svgLoadedHeight * 1.1) && + width > 0f && height > 0f && + (width != _lastArrangedBounds.Width || height != _lastArrangedBounds.Height)) + { + _lastArrangedBounds = bounds; + Console.WriteLine($"[SkiaImage] Arrange detected larger bounds: {width}x{height} vs loaded {_svgLoadedWidth}x{_svgLoadedHeight}"); + LoadSvgAtSizeAsync(_currentFilePath, width, height); + } + } + } + } + protected override SKSize MeasureOverride(SKSize availableSize) { + double widthRequest = base.WidthRequest; + double heightRequest = base.HeightRequest; + + // If both dimensions explicitly requested, use them + if (widthRequest > 0.0 && heightRequest > 0.0) + { + return new SKSize((float)widthRequest, (float)heightRequest); + } + + // If no image, return default or requested size if (_image == null) - return new SKSize(100, 100); // Default size + { + 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); + } - var imageWidth = _image.Width; - var imageHeight = _image.Height; + float imageWidth = _image.Width; + float imageHeight = _image.Height; - // If we have constraints, respect them + // If only width requested, scale height proportionally + if (widthRequest > 0.0) + { + float scale = (float)widthRequest / imageWidth; + return new SKSize((float)widthRequest, imageHeight * scale); + } + + // If only height requested, scale width proportionally + if (heightRequest > 0.0) + { + float scale = (float)heightRequest / imageHeight; + return new SKSize(imageWidth * scale, (float)heightRequest); + } + + // Scale to fit available size if (availableSize.Width < float.MaxValue && availableSize.Height < float.MaxValue) { - var scale = Math.Min(availableSize.Width / imageWidth, availableSize.Height / imageHeight); + float scale = Math.Min(availableSize.Width / imageWidth, availableSize.Height / imageHeight); return new SKSize(imageWidth * scale, imageHeight * scale); } - else if (availableSize.Width < float.MaxValue) + + if (availableSize.Width < float.MaxValue) { - var scale = availableSize.Width / imageWidth; + float scale = availableSize.Width / imageWidth; return new SKSize(availableSize.Width, imageHeight * scale); } - else if (availableSize.Height < float.MaxValue) + + if (availableSize.Height < float.MaxValue) { - var scale = availableSize.Height / imageHeight; + float scale = availableSize.Height / imageHeight; return new SKSize(imageWidth * scale, availableSize.Height); } diff --git a/Views/SkiaLabel.cs b/Views/SkiaLabel.cs index 88fa71a..9aeb473 100644 --- a/Views/SkiaLabel.cs +++ b/Views/SkiaLabel.cs @@ -25,6 +25,7 @@ public class SkiaLabel : SkiaView typeof(string), typeof(SkiaLabel), "", + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaLabel)b).OnTextChanged()); /// @@ -36,6 +37,7 @@ public class SkiaLabel : SkiaView typeof(IList), typeof(SkiaLabel), null, + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaLabel)b).OnTextChanged()); /// @@ -47,6 +49,7 @@ public class SkiaLabel : SkiaView typeof(SKColor), typeof(SkiaLabel), SKColors.Black, + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaLabel)b).Invalidate()); /// @@ -58,6 +61,7 @@ public class SkiaLabel : SkiaView typeof(string), typeof(SkiaLabel), "Sans", + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaLabel)b).OnFontChanged()); /// @@ -69,6 +73,7 @@ public class SkiaLabel : SkiaView typeof(float), typeof(SkiaLabel), 14f, + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaLabel)b).OnFontChanged()); /// @@ -80,6 +85,7 @@ public class SkiaLabel : SkiaView typeof(bool), typeof(SkiaLabel), false, + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaLabel)b).OnFontChanged()); /// @@ -91,6 +97,7 @@ public class SkiaLabel : SkiaView typeof(bool), typeof(SkiaLabel), false, + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaLabel)b).OnFontChanged()); /// @@ -102,6 +109,7 @@ public class SkiaLabel : SkiaView typeof(bool), typeof(SkiaLabel), false, + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaLabel)b).Invalidate()); /// @@ -113,6 +121,7 @@ public class SkiaLabel : SkiaView typeof(bool), typeof(SkiaLabel), false, + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaLabel)b).Invalidate()); /// @@ -124,6 +133,7 @@ public class SkiaLabel : SkiaView typeof(TextAlignment), typeof(SkiaLabel), TextAlignment.Start, + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaLabel)b).Invalidate()); /// @@ -419,7 +429,7 @@ public class SkiaLabel : SkiaView { if (System.IO.File.Exists(path)) { - _cachedTypeface = SKTypeface.FromFile(path); + _cachedTypeface = SKTypeface.FromFile(path, 0); if (_cachedTypeface != null) return _cachedTypeface; } } diff --git a/Views/SkiaPicker.cs b/Views/SkiaPicker.cs index 3348f62..18b0df2 100644 --- a/Views/SkiaPicker.cs +++ b/Views/SkiaPicker.cs @@ -21,7 +21,7 @@ public class SkiaPicker : SkiaView typeof(int), typeof(SkiaPicker), -1, - BindingMode.TwoWay, + BindingMode.OneWay, propertyChanged: (b, o, n) => ((SkiaPicker)b).OnSelectedIndexChanged()); /// @@ -33,6 +33,7 @@ public class SkiaPicker : SkiaView typeof(string), typeof(SkiaPicker), "", + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaPicker)b).Invalidate()); /// @@ -44,6 +45,7 @@ public class SkiaPicker : SkiaView typeof(SKColor), typeof(SkiaPicker), SKColors.Black, + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaPicker)b).Invalidate()); /// @@ -55,6 +57,7 @@ public class SkiaPicker : SkiaView typeof(SKColor), typeof(SkiaPicker), new SKColor(0x80, 0x80, 0x80), + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaPicker)b).Invalidate()); /// @@ -66,6 +69,7 @@ public class SkiaPicker : SkiaView typeof(SKColor), typeof(SkiaPicker), new SKColor(0xBD, 0xBD, 0xBD), + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaPicker)b).Invalidate()); /// @@ -77,6 +81,7 @@ public class SkiaPicker : SkiaView typeof(SKColor), typeof(SkiaPicker), SKColors.White, + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaPicker)b).Invalidate()); /// @@ -88,6 +93,7 @@ public class SkiaPicker : SkiaView typeof(SKColor), typeof(SkiaPicker), new SKColor(0x21, 0x96, 0xF3, 0x30), + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaPicker)b).Invalidate()); /// @@ -99,6 +105,7 @@ public class SkiaPicker : SkiaView typeof(SKColor), typeof(SkiaPicker), new SKColor(0xE0, 0xE0, 0xE0), + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaPicker)b).Invalidate()); /// @@ -110,6 +117,7 @@ public class SkiaPicker : SkiaView typeof(string), typeof(SkiaPicker), "Sans", + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaPicker)b).InvalidateMeasure()); /// @@ -121,6 +129,7 @@ public class SkiaPicker : SkiaView typeof(float), typeof(SkiaPicker), 14f, + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaPicker)b).InvalidateMeasure()); /// @@ -132,6 +141,7 @@ public class SkiaPicker : SkiaView typeof(float), typeof(SkiaPicker), 40f, + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaPicker)b).Invalidate()); /// @@ -143,6 +153,7 @@ public class SkiaPicker : SkiaView typeof(float), typeof(SkiaPicker), 4f, + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaPicker)b).Invalidate()); #endregion diff --git a/Views/SkiaProgressBar.cs b/Views/SkiaProgressBar.cs index ea7f7b6..3c3ee32 100644 --- a/Views/SkiaProgressBar.cs +++ b/Views/SkiaProgressBar.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; +using Microsoft.Maui.Controls; using SkiaSharp; namespace Microsoft.Maui.Platform; @@ -21,8 +23,8 @@ public class SkiaProgressBar : SkiaView typeof(double), typeof(SkiaProgressBar), 0.0, - BindingMode.TwoWay, - coerceValue: (b, v) => Math.Clamp((double)v, 0, 1), + BindingMode.OneWay, + coerceValue: (b, v) => Math.Clamp((double)v, 0.0, 1.0), propertyChanged: (b, o, n) => ((SkiaProgressBar)b).OnProgressChanged()); /// @@ -33,7 +35,8 @@ public class SkiaProgressBar : SkiaView nameof(TrackColor), typeof(SKColor), typeof(SkiaProgressBar), - new SKColor(0xE0, 0xE0, 0xE0), + new SKColor(224, 224, 224), + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaProgressBar)b).Invalidate()); /// @@ -44,7 +47,8 @@ public class SkiaProgressBar : SkiaView nameof(ProgressColor), typeof(SKColor), typeof(SkiaProgressBar), - new SKColor(0x21, 0x96, 0xF3), + new SKColor(33, 150, 243), + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaProgressBar)b).Invalidate()); /// @@ -55,7 +59,8 @@ public class SkiaProgressBar : SkiaView nameof(DisabledColor), typeof(SKColor), typeof(SkiaProgressBar), - new SKColor(0xBD, 0xBD, 0xBD), + new SKColor(189, 189, 189), + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaProgressBar)b).Invalidate()); /// @@ -67,6 +72,7 @@ public class SkiaProgressBar : SkiaView typeof(float), typeof(SkiaProgressBar), 4f, + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaProgressBar)b).InvalidateMeasure()); /// @@ -78,6 +84,7 @@ public class SkiaProgressBar : SkiaView typeof(float), typeof(SkiaProgressBar), 2f, + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaProgressBar)b).Invalidate()); #endregion @@ -153,9 +160,9 @@ public class SkiaProgressBar : SkiaView protected override void OnDraw(SKCanvas canvas, SKRect bounds) { - var trackY = bounds.MidY; - var trackTop = trackY - BarHeight / 2; - var trackBottom = trackY + BarHeight / 2; + float midY = bounds.MidY; + float trackTop = midY - BarHeight / 2f; + float trackBottom = midY + BarHeight / 2f; // Draw track using var trackPaint = new SKPaint @@ -171,9 +178,9 @@ public class SkiaProgressBar : SkiaView canvas.DrawRoundRect(trackRect, trackPaint); // Draw progress - if (Progress > 0) + if (Progress > 0.0) { - var progressWidth = bounds.Width * (float)Progress; + float progressWidth = bounds.Width * (float)Progress; using var progressPaint = new SKPaint { @@ -191,7 +198,7 @@ public class SkiaProgressBar : SkiaView protected override SKSize MeasureOverride(SKSize availableSize) { - return new SKSize(200, BarHeight + 8); + return new SKSize(200f, BarHeight + 8f); } } diff --git a/Views/SkiaSlider.cs b/Views/SkiaSlider.cs index 9b30e74..6ed6124 100644 --- a/Views/SkiaSlider.cs +++ b/Views/SkiaSlider.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; +using Microsoft.Maui.Controls; using SkiaSharp; namespace Microsoft.Maui.Platform; @@ -21,6 +23,7 @@ public class SkiaSlider : SkiaView typeof(double), typeof(SkiaSlider), 0.0, + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaSlider)b).OnRangeChanged()); /// @@ -32,6 +35,7 @@ public class SkiaSlider : SkiaView typeof(double), typeof(SkiaSlider), 100.0, + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaSlider)b).OnRangeChanged()); /// @@ -43,7 +47,7 @@ public class SkiaSlider : SkiaView typeof(double), typeof(SkiaSlider), 0.0, - BindingMode.TwoWay, + BindingMode.OneWay, propertyChanged: (b, o, n) => ((SkiaSlider)b).OnValuePropertyChanged((double)o, (double)n)); /// @@ -55,6 +59,7 @@ public class SkiaSlider : SkiaView typeof(SKColor), typeof(SkiaSlider), new SKColor(0xE0, 0xE0, 0xE0), + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaSlider)b).Invalidate()); /// @@ -66,6 +71,7 @@ public class SkiaSlider : SkiaView typeof(SKColor), typeof(SkiaSlider), new SKColor(0x21, 0x96, 0xF3), + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaSlider)b).Invalidate()); /// @@ -77,6 +83,7 @@ public class SkiaSlider : SkiaView typeof(SKColor), typeof(SkiaSlider), new SKColor(0x21, 0x96, 0xF3), + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaSlider)b).Invalidate()); /// @@ -88,6 +95,7 @@ public class SkiaSlider : SkiaView typeof(SKColor), typeof(SkiaSlider), new SKColor(0xBD, 0xBD, 0xBD), + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaSlider)b).Invalidate()); /// @@ -99,6 +107,7 @@ public class SkiaSlider : SkiaView typeof(float), typeof(SkiaSlider), 4f, + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaSlider)b).Invalidate()); /// @@ -110,6 +119,7 @@ public class SkiaSlider : SkiaView typeof(float), typeof(SkiaSlider), 10f, + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaSlider)b).InvalidateMeasure()); #endregion @@ -318,7 +328,7 @@ public class SkiaSlider : SkiaView _isDragging = true; UpdateValueFromPosition(e.X); DragStarted?.Invoke(this, EventArgs.Empty); - SkiaVisualStateManager.GoToState(this, SkiaVisualStateManager.CommonStates.Pressed); + SkiaVisualStateManager.GoToState(this, "Pressed"); } public override void OnPointerMoved(PointerEventArgs e) @@ -333,7 +343,7 @@ public class SkiaSlider : SkiaView { _isDragging = false; DragCompleted?.Invoke(this, EventArgs.Empty); - SkiaVisualStateManager.GoToState(this, IsEnabled ? SkiaVisualStateManager.CommonStates.Normal : SkiaVisualStateManager.CommonStates.Disabled); + SkiaVisualStateManager.GoToState(this, IsEnabled ? "Normal" : "Disabled"); } } @@ -379,7 +389,7 @@ public class SkiaSlider : SkiaView protected override void OnEnabledChanged() { base.OnEnabledChanged(); - SkiaVisualStateManager.GoToState(this, IsEnabled ? SkiaVisualStateManager.CommonStates.Normal : SkiaVisualStateManager.CommonStates.Disabled); + SkiaVisualStateManager.GoToState(this, IsEnabled ? "Normal" : "Disabled"); } protected override SKSize MeasureOverride(SKSize availableSize) diff --git a/Views/SkiaSwitch.cs b/Views/SkiaSwitch.cs index 0120307..cc7e335 100644 --- a/Views/SkiaSwitch.cs +++ b/Views/SkiaSwitch.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; +using Microsoft.Maui.Controls; using SkiaSharp; namespace Microsoft.Maui.Platform; @@ -21,7 +23,7 @@ public class SkiaSwitch : SkiaView typeof(bool), typeof(SkiaSwitch), false, - BindingMode.TwoWay, + BindingMode.OneWay, propertyChanged: (b, o, n) => ((SkiaSwitch)b).OnIsOnChanged()); /// @@ -32,7 +34,8 @@ public class SkiaSwitch : SkiaView nameof(OnTrackColor), typeof(SKColor), typeof(SkiaSwitch), - new SKColor(0x21, 0x96, 0xF3), + new SKColor(33, 150, 243), + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaSwitch)b).Invalidate()); /// @@ -43,7 +46,8 @@ public class SkiaSwitch : SkiaView nameof(OffTrackColor), typeof(SKColor), typeof(SkiaSwitch), - new SKColor(0x9E, 0x9E, 0x9E), + new SKColor(158, 158, 158), + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaSwitch)b).Invalidate()); /// @@ -55,6 +59,7 @@ public class SkiaSwitch : SkiaView typeof(SKColor), typeof(SkiaSwitch), SKColors.White, + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaSwitch)b).Invalidate()); /// @@ -65,7 +70,8 @@ public class SkiaSwitch : SkiaView nameof(DisabledColor), typeof(SKColor), typeof(SkiaSwitch), - new SKColor(0xBD, 0xBD, 0xBD), + new SKColor(189, 189, 189), + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaSwitch)b).Invalidate()); /// @@ -77,6 +83,7 @@ public class SkiaSwitch : SkiaView typeof(float), typeof(SkiaSwitch), 52f, + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaSwitch)b).InvalidateMeasure()); /// @@ -88,6 +95,7 @@ public class SkiaSwitch : SkiaView typeof(float), typeof(SkiaSwitch), 32f, + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaSwitch)b).InvalidateMeasure()); /// @@ -99,6 +107,7 @@ public class SkiaSwitch : SkiaView typeof(float), typeof(SkiaSwitch), 12f, + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaSwitch)b).Invalidate()); /// @@ -110,6 +119,7 @@ public class SkiaSwitch : SkiaView typeof(float), typeof(SkiaSwitch), 4f, + BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaSwitch)b).Invalidate()); #endregion @@ -199,7 +209,7 @@ public class SkiaSwitch : SkiaView #endregion - private float _animationProgress; // 0 = off, 1 = on + private float _animationProgress; /// /// Event raised when the switch is toggled. @@ -215,14 +225,14 @@ public class SkiaSwitch : SkiaView { _animationProgress = IsOn ? 1f : 0f; Toggled?.Invoke(this, new ToggledEventArgs(IsOn)); - SkiaVisualStateManager.GoToState(this, IsOn ? SkiaVisualStateManager.CommonStates.On : SkiaVisualStateManager.CommonStates.Off); + SkiaVisualStateManager.GoToState(this, IsOn ? "On" : "Off"); Invalidate(); } protected override void OnDraw(SKCanvas canvas, SKRect bounds) { var centerY = bounds.MidY; - var trackLeft = bounds.MidX - TrackWidth / 2; + var trackLeft = bounds.MidX - TrackWidth / 2f; var trackRight = trackLeft + TrackWidth; // Calculate thumb position @@ -244,8 +254,8 @@ public class SkiaSwitch : SkiaView }; var trackRect = new SKRoundRect( - new SKRect(trackLeft, centerY - TrackHeight / 2, trackRight, centerY + TrackHeight / 2), - TrackHeight / 2); + new SKRect(trackLeft, centerY - TrackHeight / 2f, trackRight, centerY + TrackHeight / 2f), + TrackHeight / 2f); canvas.DrawRoundRect(trackRect, trackPaint); // Draw thumb shadow @@ -255,15 +265,15 @@ public class SkiaSwitch : SkiaView { Color = new SKColor(0, 0, 0, 40), IsAntialias = true, - MaskFilter = SKMaskFilter.CreateBlur(SKBlurStyle.Normal, 2) + MaskFilter = SKMaskFilter.CreateBlur(SKBlurStyle.Normal, 2f) }; - canvas.DrawCircle(thumbX + 1, centerY + 1, ThumbRadius, shadowPaint); + canvas.DrawCircle(thumbX + 1f, centerY + 1f, ThumbRadius, shadowPaint); } // Draw thumb using var thumbPaint = new SKPaint { - Color = IsEnabled ? ThumbColor : new SKColor(0xF5, 0xF5, 0xF5), + Color = IsEnabled ? ThumbColor : new SKColor(245, 245, 245), IsAntialias = true, Style = SKPaintStyle.Fill }; @@ -277,10 +287,10 @@ public class SkiaSwitch : SkiaView Color = OnTrackColor.WithAlpha(60), IsAntialias = true, Style = SKPaintStyle.Stroke, - StrokeWidth = 3 + StrokeWidth = 3f }; - var focusRect = new SKRoundRect(trackRect.Rect, TrackHeight / 2); - focusRect.Inflate(3, 3); + var focusRect = new SKRoundRect(trackRect.Rect, TrackHeight / 2f); + focusRect.Inflate(3f, 3f); canvas.DrawRoundRect(focusRect, focusPaint); } } @@ -296,21 +306,20 @@ public class SkiaSwitch : SkiaView public override void OnPointerPressed(PointerEventArgs e) { - if (!IsEnabled) return; - IsOn = !IsOn; - e.Handled = true; + if (IsEnabled) + { + 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) + if (IsEnabled && (e.Key == Key.Space || e.Key == Key.Enter)) { IsOn = !IsOn; e.Handled = true; @@ -320,20 +329,11 @@ public class SkiaSwitch : SkiaView protected override void OnEnabledChanged() { base.OnEnabledChanged(); - SkiaVisualStateManager.GoToState(this, IsEnabled ? SkiaVisualStateManager.CommonStates.Normal : SkiaVisualStateManager.CommonStates.Disabled); + SkiaVisualStateManager.GoToState(this, IsEnabled ? "Normal" : "Disabled"); } protected override SKSize MeasureOverride(SKSize availableSize) { - return new SKSize(TrackWidth + 8, TrackHeight + 8); + return new SKSize(TrackWidth + 8f, TrackHeight + 8f); } } - -/// -/// Event args for toggled events. -/// -public class ToggledEventArgs : EventArgs -{ - public bool Value { get; } - public ToggledEventArgs(bool value) => Value = value; -}