// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using SkiaSharp; using Microsoft.Maui.Platform.Linux.Rendering; namespace Microsoft.Maui.Platform; /// /// Skia-rendered checkbox control with full XAML styling support. /// public class SkiaCheckBox : SkiaView { #region BindableProperties /// /// Bindable property for IsChecked. /// public static readonly BindableProperty IsCheckedProperty = BindableProperty.Create( nameof(IsChecked), typeof(bool), typeof(SkiaCheckBox), false, BindingMode.TwoWay, propertyChanged: (b, o, n) => ((SkiaCheckBox)b).OnIsCheckedChanged()); /// /// Bindable property for CheckColor. /// public static readonly BindableProperty CheckColorProperty = BindableProperty.Create( nameof(CheckColor), typeof(SKColor), typeof(SkiaCheckBox), SKColors.White, propertyChanged: (b, o, n) => ((SkiaCheckBox)b).Invalidate()); /// /// Bindable property for BoxColor. /// public static readonly BindableProperty BoxColorProperty = BindableProperty.Create( nameof(BoxColor), typeof(SKColor), typeof(SkiaCheckBox), new SKColor(0x21, 0x96, 0xF3), propertyChanged: (b, o, n) => ((SkiaCheckBox)b).Invalidate()); /// /// Bindable property for UncheckedBoxColor. /// public static readonly BindableProperty UncheckedBoxColorProperty = BindableProperty.Create( nameof(UncheckedBoxColor), typeof(SKColor), typeof(SkiaCheckBox), SKColors.White, propertyChanged: (b, o, n) => ((SkiaCheckBox)b).Invalidate()); /// /// Bindable property for BorderColor. /// public static readonly BindableProperty BorderColorProperty = BindableProperty.Create( nameof(BorderColor), typeof(SKColor), typeof(SkiaCheckBox), new SKColor(0x75, 0x75, 0x75), propertyChanged: (b, o, n) => ((SkiaCheckBox)b).Invalidate()); /// /// Bindable property for DisabledColor. /// public static readonly BindableProperty DisabledColorProperty = BindableProperty.Create( nameof(DisabledColor), typeof(SKColor), typeof(SkiaCheckBox), new SKColor(0xBD, 0xBD, 0xBD), propertyChanged: (b, o, n) => ((SkiaCheckBox)b).Invalidate()); /// /// Bindable property for HoveredBorderColor. /// public static readonly BindableProperty HoveredBorderColorProperty = BindableProperty.Create( nameof(HoveredBorderColor), typeof(SKColor), typeof(SkiaCheckBox), new SKColor(0x21, 0x96, 0xF3), propertyChanged: (b, o, n) => ((SkiaCheckBox)b).Invalidate()); /// /// Bindable property for BoxSize. /// public static readonly BindableProperty BoxSizeProperty = BindableProperty.Create( nameof(BoxSize), typeof(float), typeof(SkiaCheckBox), 20f, propertyChanged: (b, o, n) => ((SkiaCheckBox)b).InvalidateMeasure()); /// /// Bindable property for CornerRadius. /// public static readonly BindableProperty CornerRadiusProperty = BindableProperty.Create( nameof(CornerRadius), typeof(float), typeof(SkiaCheckBox), 3f, propertyChanged: (b, o, n) => ((SkiaCheckBox)b).Invalidate()); /// /// Bindable property for BorderWidth. /// public static readonly BindableProperty BorderWidthProperty = BindableProperty.Create( nameof(BorderWidth), typeof(float), typeof(SkiaCheckBox), 2f, propertyChanged: (b, o, n) => ((SkiaCheckBox)b).Invalidate()); /// /// Bindable property for CheckStrokeWidth. /// public static readonly BindableProperty CheckStrokeWidthProperty = BindableProperty.Create( nameof(CheckStrokeWidth), typeof(float), typeof(SkiaCheckBox), 2.5f, propertyChanged: (b, o, n) => ((SkiaCheckBox)b).Invalidate()); #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() { IsFocusable = true; } private void OnIsCheckedChanged() { CheckedChanged?.Invoke(this, new CheckedChangedEventArgs(IsChecked)); SkiaVisualStateManager.GoToState(this, IsChecked ? SkiaVisualStateManager.CommonStates.Checked : SkiaVisualStateManager.CommonStates.Unchecked); Invalidate(); } protected override void OnDraw(SKCanvas canvas, SKRect bounds) { // Center the checkbox box in bounds var boxRect = new SKRect( bounds.Left + (bounds.Width - BoxSize) / 2, bounds.Top + (bounds.Height - BoxSize) / 2, bounds.Left + (bounds.Width - BoxSize) / 2 + BoxSize, bounds.Top + (bounds.Height - BoxSize) / 2 + BoxSize); var roundRect = new SKRoundRect(boxRect, CornerRadius); // Draw background using var bgPaint = new SKPaint { Color = !IsEnabled ? DisabledColor : IsChecked ? BoxColor : UncheckedBoxColor, IsAntialias = true, Style = SKPaintStyle.Fill }; canvas.DrawRoundRect(roundRect, bgPaint); // Draw border using var borderPaint = new SKPaint { Color = !IsEnabled ? DisabledColor : IsChecked ? BoxColor : IsHovered ? HoveredBorderColor : BorderColor, IsAntialias = true, Style = SKPaintStyle.Stroke, StrokeWidth = BorderWidth }; canvas.DrawRoundRect(roundRect, borderPaint); // Draw focus ring if (IsFocused) { using var focusPaint = new SKPaint { Color = BoxColor.WithAlpha(80), IsAntialias = true, Style = SKPaintStyle.Stroke, StrokeWidth = 3 }; var focusRect = new SKRoundRect(boxRect, CornerRadius); focusRect.Inflate(4, 4); canvas.DrawRoundRect(focusRect, focusPaint); } // Draw checkmark if (IsChecked) { DrawCheckmark(canvas, boxRect); } } private void DrawCheckmark(SKCanvas canvas, SKRect boxRect) { using var paint = new SKPaint { Color = CheckColor, IsAntialias = true, Style = SKPaintStyle.Stroke, StrokeWidth = CheckStrokeWidth, StrokeCap = SKStrokeCap.Round, StrokeJoin = SKStrokeJoin.Round }; // Checkmark path - a simple check var padding = BoxSize * 0.2f; var left = boxRect.Left + padding; var right = boxRect.Right - padding; var top = boxRect.Top + padding; var bottom = boxRect.Bottom - padding; // Check starts from bottom-left, goes to middle-bottom, then to top-right using var path = new SKPath(); path.MoveTo(left, boxRect.MidY); path.LineTo(boxRect.MidX - padding * 0.3f, bottom - padding * 0.5f); path.LineTo(right, top + padding * 0.3f); canvas.DrawPath(path, paint); } public override void OnPointerEntered(PointerEventArgs e) { if (!IsEnabled) return; IsHovered = true; SkiaVisualStateManager.GoToState(this, SkiaVisualStateManager.CommonStates.PointerOver); Invalidate(); } public override void OnPointerExited(PointerEventArgs e) { IsHovered = false; SkiaVisualStateManager.GoToState(this, IsEnabled ? SkiaVisualStateManager.CommonStates.Normal : SkiaVisualStateManager.CommonStates.Disabled); Invalidate(); } public override void OnPointerPressed(PointerEventArgs e) { if (!IsEnabled) return; IsChecked = !IsChecked; e.Handled = true; } public override void OnPointerReleased(PointerEventArgs e) { // Toggle handled in OnPointerPressed } public override void OnKeyDown(KeyEventArgs e) { if (!IsEnabled) return; // Toggle on Space if (e.Key == Key.Space) { IsChecked = !IsChecked; e.Handled = true; } } protected override void OnEnabledChanged() { base.OnEnabledChanged(); SkiaVisualStateManager.GoToState(this, IsEnabled ? SkiaVisualStateManager.CommonStates.Normal : SkiaVisualStateManager.CommonStates.Disabled); } protected override SKSize MeasureOverride(SKSize availableSize) { // Add some padding around the box for touch targets return new SKSize(BoxSize + 8, BoxSize + 8); } } /// /// Event args for checked changed events. /// public class CheckedChangedEventArgs : EventArgs { public bool IsChecked { get; } public CheckedChangedEventArgs(bool isChecked) { IsChecked = isChecked; } }