// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using SkiaSharp; namespace Microsoft.Maui.Platform; /// /// Skia-rendered radio button control. /// public class SkiaRadioButton : SkiaView { private bool _isChecked; private string _content = ""; private object? _value; private string? _groupName; // Styling public SKColor RadioColor { get; set; } = new SKColor(0x21, 0x96, 0xF3); public SKColor UncheckedColor { get; set; } = new SKColor(0x75, 0x75, 0x75); public SKColor TextColor { get; set; } = SKColors.Black; public SKColor DisabledColor { get; set; } = new SKColor(0xBD, 0xBD, 0xBD); public float FontSize { get; set; } = 14; public float RadioSize { get; set; } = 20; public float Spacing { get; set; } = 8; // Static group management private static readonly Dictionary>> _groups = new(); public bool IsChecked { get => _isChecked; set { if (_isChecked != value) { _isChecked = value; if (_isChecked && !string.IsNullOrEmpty(_groupName)) { UncheckOthersInGroup(); } CheckedChanged?.Invoke(this, EventArgs.Empty); Invalidate(); } } } public string Content { get => _content; set { _content = value ?? ""; Invalidate(); } } public object? Value { get => _value; set { _value = value; } } public string? GroupName { get => _groupName; set { if (_groupName != value) { RemoveFromGroup(); _groupName = value; AddToGroup(); } } } public event EventHandler? CheckedChanged; public SkiaRadioButton() { IsFocusable = true; } private void AddToGroup() { if (string.IsNullOrEmpty(_groupName)) return; if (!_groups.TryGetValue(_groupName, out var group)) { group = new List>(); _groups[_groupName] = group; } // Clean up dead references and add this one group.RemoveAll(wr => !wr.TryGetTarget(out _)); group.Add(new WeakReference(this)); } private void RemoveFromGroup() { if (string.IsNullOrEmpty(_groupName)) return; if (_groups.TryGetValue(_groupName, out var group)) { group.RemoveAll(wr => !wr.TryGetTarget(out var target) || target == this); if (group.Count == 0) { _groups.Remove(_groupName); } } } private void UncheckOthersInGroup() { if (string.IsNullOrEmpty(_groupName)) return; if (_groups.TryGetValue(_groupName, out var group)) { foreach (var weakRef in group) { if (weakRef.TryGetTarget(out var radioButton) && radioButton != this) { radioButton._isChecked = false; radioButton.CheckedChanged?.Invoke(radioButton, EventArgs.Empty); radioButton.Invalidate(); } } } } protected override void OnDraw(SKCanvas canvas, SKRect bounds) { var radioRadius = RadioSize / 2; var radioCenterX = bounds.Left + radioRadius; var radioCenterY = bounds.MidY; // Draw outer circle using var outerPaint = new SKPaint { Color = IsEnabled ? (_isChecked ? RadioColor : UncheckedColor) : DisabledColor, Style = SKPaintStyle.Stroke, StrokeWidth = 2, IsAntialias = true }; canvas.DrawCircle(radioCenterX, radioCenterY, radioRadius - 1, outerPaint); // Draw inner circle if checked if (_isChecked) { using var innerPaint = new SKPaint { Color = IsEnabled ? RadioColor : DisabledColor, Style = SKPaintStyle.Fill, IsAntialias = true }; canvas.DrawCircle(radioCenterX, radioCenterY, radioRadius - 5, innerPaint); } // Draw focus ring if (IsFocused) { using var focusPaint = new SKPaint { Color = RadioColor.WithAlpha(80), Style = SKPaintStyle.Fill, IsAntialias = true }; canvas.DrawCircle(radioCenterX, radioCenterY, radioRadius + 4, focusPaint); } // Draw content text if (!string.IsNullOrEmpty(_content)) { using var font = new SKFont(SKTypeface.Default, FontSize); using var textPaint = new SKPaint(font) { Color = IsEnabled ? TextColor : DisabledColor, IsAntialias = true }; var textX = bounds.Left + RadioSize + Spacing; var textBounds = new SKRect(); textPaint.MeasureText(_content, ref textBounds); canvas.DrawText(_content, textX, bounds.MidY - textBounds.MidY, textPaint); } } public override void OnPointerPressed(PointerEventArgs e) { if (!IsEnabled) return; if (!_isChecked) { IsChecked = true; } } public override void OnKeyDown(KeyEventArgs e) { if (!IsEnabled) return; switch (e.Key) { case Key.Space: case Key.Enter: if (!_isChecked) { IsChecked = true; } e.Handled = true; break; } } protected override SKSize MeasureOverride(SKSize availableSize) { var textWidth = 0f; if (!string.IsNullOrEmpty(_content)) { using var font = new SKFont(SKTypeface.Default, FontSize); using var paint = new SKPaint(font); textWidth = paint.MeasureText(_content) + Spacing; } return new SKSize(RadioSize + textWidth, Math.Max(RadioSize, FontSize * 1.5f)); } }