using System; using System.Collections.Generic; using System.Linq; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Layout; using Avalonia.Media; using Avalonia.VisualTree; using FluentIcons.Avalonia.Fluent; using FluentIcons.Common; namespace StabilityMatrix.Avalonia.Controls; public class StarsRating : TemplatedControl { private SymbolIcon? StarFilledIcon => Resources["StarFilledIcon"] as SymbolIcon; private ItemsControl? itemsControl; private IEnumerable StarItems => itemsControl!.ItemsSource!.Cast(); public static readonly StyledProperty IsEditableProperty = AvaloniaProperty.Register< StarsRating, bool >("IsEditable"); public bool IsEditable { get => GetValue(IsEditableProperty); set => SetValue(IsEditableProperty, value); } public static readonly StyledProperty MaximumProperty = AvaloniaProperty.Register( nameof(Maximum), 5 ); public int Maximum { get => GetValue(MaximumProperty); set => SetValue(MaximumProperty, value); } public static readonly StyledProperty ValueProperty = AvaloniaProperty.Register< StarsRating, double >(nameof(Value)); public double Value { get => GetValue(ValueProperty); set => SetValue(ValueProperty, value); } /// protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); itemsControl = e.NameScope.Find("PART_StarsItemsControl")!; CreateStars(); } /// protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); if (!this.IsAttachedToVisualTree()) { return; } if (change.Property == ValueProperty || change.Property == MaximumProperty) { SyncStarState(); } } private void CreateStars() { if (itemsControl is null) { return; } // Fill stars var stars = new List(); for (var i = 0; i < Maximum; i++) { var star = new SymbolIcon { FontSize = FontSize, Margin = new Thickness(0, 0), Symbol = Symbol.Star, HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center, Tag = i }; stars.Add(star); OnStarAdded(star); } itemsControl.ItemsSource = stars; SyncStarState(); } private void OnStarAdded(SymbolIcon item) { if (IsEditable) { item.Tapped += (sender, args) => { var star = (SymbolIcon)sender!; Value = (int)star.Tag! + 1; }; } } /// /// Round a number to the nearest 0.5 /// private static double RoundToHalf(double value) { return Math.Round(value * 2, MidpointRounding.AwayFromZero) / 2; } private void SyncStarState() { // Set star to filled when Value is greater than or equal to the star index foreach (var star in StarItems) { // Add 1 to tag since its index is 0-based var tag = (int)star.Tag! + 1; // Fill if current is equal or lower than floor of Value if (tag <= Math.Floor(RoundToHalf(Value))) { star.Symbol = Symbol.Star; star.IsFilled = true; star.Foreground = Foreground; } // If current is between floor and ceil of value, use half-star else if (tag <= Math.Ceiling(RoundToHalf(Value))) { star.Symbol = Symbol.StarHalf; star.IsFilled = true; star.Foreground = Foreground; } // Otherwise no fill and gray disabled color else { star.Symbol = Symbol.Star; star.IsFilled = false; star.Foreground = new SolidColorBrush(Colors.DarkSlateGray); } } } }