using System.Diagnostics; using AtomUI.Controls.Utils; using AtomUI.Data; using AtomUI.Theme; using AtomUI.Theme.Utils; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Layout; using Avalonia.LogicalTree; using Avalonia.Media; namespace AtomUI.Controls; public class OptionCheckedChangedEventArgs : RoutedEventArgs { public OptionCheckedChangedEventArgs(RoutedEvent routedEvent, OptionButton option, int index) : base(routedEvent) { CheckedOption = option; Index = index; } public OptionButton CheckedOption { get; } public int Index { get; } } public class OptionButtonGroup : SelectingItemsControl, ISizeTypeAware, IAnimationAwareControl, IControlSharedTokenResourcesHost { #region 公共属性定义 public static readonly StyledProperty SizeTypeProperty = SizeTypeAwareControlProperty.SizeTypeProperty.AddOwner(); public static readonly StyledProperty ButtonStyleProperty = AvaloniaProperty.Register(nameof(SizeType)); public static readonly StyledProperty IsMotionEnabledProperty = AnimationAwareControlProperty.IsMotionEnabledProperty.AddOwner(); public static readonly StyledProperty IsWaveAnimationEnabledProperty = AnimationAwareControlProperty.IsWaveAnimationEnabledProperty.AddOwner(); public static readonly RoutedEvent OptionCheckedChangedEvent = RoutedEvent.Register( nameof(OptionCheckedChanged), RoutingStrategies.Bubble); public SizeType SizeType { get => GetValue(SizeTypeProperty); set => SetValue(SizeTypeProperty, value); } public OptionButtonStyle ButtonStyle { get => GetValue(ButtonStyleProperty); set => SetValue(ButtonStyleProperty, value); } public bool IsMotionEnabled { get => GetValue(IsMotionEnabledProperty); set => SetValue(IsMotionEnabledProperty, value); } public bool IsWaveAnimationEnabled { get => GetValue(IsWaveAnimationEnabledProperty); set => SetValue(IsWaveAnimationEnabledProperty, value); } public event EventHandler? OptionCheckedChanged { add => AddHandler(OptionCheckedChangedEvent, value); remove => RemoveHandler(OptionCheckedChangedEvent, value); } #endregion #region 内部属性定义 internal static readonly DirectProperty EffectiveBorderThicknessProperty = AvaloniaProperty.RegisterDirect(nameof(EffectiveBorderThickness), o => o.EffectiveBorderThickness, (o, v) => o.EffectiveBorderThickness = v); internal static readonly StyledProperty SelectedOptionBorderColorProperty = AvaloniaProperty.Register(nameof(SelectedOptionBorderColor)); private Thickness _effectiveBorderThickness; internal Thickness EffectiveBorderThickness { get => _effectiveBorderThickness; set => SetAndRaise(EffectiveBorderThicknessProperty, ref _effectiveBorderThickness, value); } internal IBrush? SelectedOptionBorderColor { get => GetValue(SelectedOptionBorderColorProperty); set => SetValue(SelectedOptionBorderColorProperty, value); } private static readonly FuncTemplate DefaultPanel = new(() => new StackPanel { Orientation = Orientation.Horizontal }); Control IAnimationAwareControl.PropertyBindTarget => this; Control IControlSharedTokenResourcesHost.HostControl => this; string IControlSharedTokenResourcesHost.TokenId => OptionButtonToken.ID; #endregion private readonly BorderRenderHelper _borderRenderHelper = new(); static OptionButtonGroup() { SelectionModeProperty.OverrideDefaultValue(SelectionMode.Single | SelectionMode.AlwaysSelected); ItemsPanelProperty.OverrideDefaultValue(DefaultPanel); AffectsRender(SelectionModeProperty); AffectsMeasure(SizeTypeProperty); AffectsRender(SelectedOptionBorderColorProperty, ButtonStyleProperty, SelectedItemProperty); } public OptionButtonGroup() { this.RegisterResources(); this.BindAnimationProperties(IsMotionEnabledProperty, IsWaveAnimationEnabledProperty); if (this is IChildIndexProvider childIndexProvider) { childIndexProvider.ChildIndexChanged += (sender, args) => { UpdateOptionButtonsPosition(); }; } } protected override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey) { return new OptionButton(); } protected override void OnGotFocus(GotFocusEventArgs e) { base.OnGotFocus(e); if (e.NavigationMethod == NavigationMethod.Directional && e.Source is OptionButton) { e.Handled = UpdateSelectionFromEventSource(e.Source); } } protected override void PrepareContainerForItemOverride(Control element, object? item, int index) { base.PrepareContainerForItemOverride(element, item, index); if (item is OptionButton optionButton) { BindUtils.RelayBind(this, SizeTypeProperty, optionButton, OptionButton.SizeTypeProperty); BindUtils.RelayBind(this, ButtonStyleProperty, optionButton, OptionButton.ButtonStyleProperty); BindUtils.RelayBind(this, IsMotionEnabledProperty, optionButton, OptionButton.IsMotionEnabledProperty); BindUtils.RelayBind(this, IsWaveAnimationEnabledProperty, optionButton, OptionButton.IsWaveAnimationEnabledProperty); optionButton.IsCheckedChanged += HandleOptionButtonChecked; } } protected override void ContainerForItemPreparedOverride(Control container, object? item, int index) { base.ContainerForItemPreparedOverride(container, item, index); if (container is OptionButton optionButton) { if (optionButton.IsChecked.HasValue && optionButton.IsChecked.Value) { UpdateSelectionFromEventSource(optionButton); RaiseEvent(new OptionCheckedChangedEventArgs(OptionCheckedChangedEvent, optionButton, index)); } } } private void HandleOptionButtonChecked(object? sender, RoutedEventArgs args) { if (sender is OptionButton optionButton && optionButton.IsChecked.HasValue && optionButton.IsChecked.Value) { UpdateSelectionFromEventSource(args.Source); RaiseEvent(new OptionCheckedChangedEventArgs(OptionCheckedChangedEvent, optionButton, SelectedIndex)); } } protected override bool NeedsContainerOverride(object? item, int index, out object? recycleKey) { return NeedsContainer(item, out recycleKey); } protected override void ContainerIndexChangedOverride(Control container, int oldIndex, int newIndex) { if (container is OptionButton optionButton) { if (newIndex == 0) { optionButton.GroupPositionTrait = OptionButtonPositionTrait.First; } else if (newIndex == ItemCount - 1) { optionButton.GroupPositionTrait = OptionButtonPositionTrait.Last; } else { optionButton.GroupPositionTrait = OptionButtonPositionTrait.Middle; } } } private void UpdateOptionButtonsPosition() { for (var i = 0; i < Items.Count; i++) { var button = Items[i] as OptionButton; Debug.Assert(button != null); if (Items.Count > 1) { if (i == 0) { button.GroupPositionTrait = OptionButtonPositionTrait.First; } else if (i == Items.Count - 1) { button.GroupPositionTrait = OptionButtonPositionTrait.Last; } else { button.GroupPositionTrait = OptionButtonPositionTrait.Middle; } } } } public override void Render(DrawingContext context) { var borderThickness = BorderUtils.BuildRenderScaleAwareThickness(BorderThickness, VisualRoot?.RenderScaling ?? 1.0); _borderRenderHelper.Render(context, new Size(DesiredSize.Width, DesiredSize.Height), new Thickness(1), CornerRadius, BackgroundSizing.CenterBorder, null, BorderBrush, new BoxShadows()); for (var i = 0; i < ItemCount; ++i) { var optionButton = ContainerFromIndex(i); Debug.Assert(optionButton != null); if (ButtonStyle == OptionButtonStyle.Solid) { if (i <= ItemCount - 2) { var nextOption = ContainerFromIndex(i + 1); if (nextOption == SelectedItem || optionButton == SelectedItem) { continue; } } } if (i != ItemCount - 1) { var offsetX = optionButton.Bounds.Right - borderThickness.Left / 2; var startPoint = new Point(offsetX, 0); var endPoint = new Point(offsetX, Bounds.Height); using var optionState = context.PushRenderOptions(new RenderOptions { EdgeMode = EdgeMode.Aliased }); context.DrawLine(new Pen(BorderBrush, borderThickness.Left), startPoint, endPoint); } if (ButtonStyle == OptionButtonStyle.Outline) { if (optionButton.IsEnabled && optionButton == SelectedItem) { // 绘制选中边框 var offsetX = optionButton.Bounds.X; var width = optionButton.DesiredSize.Width; if (i > 0) { offsetX -= borderThickness.Left; width += borderThickness.Left; } var translationMatrix = Matrix.CreateTranslation(offsetX, 0); using var state = context.PushTransform(translationMatrix); var cornerRadius = new CornerRadius(0); if (i == 0) { cornerRadius = new CornerRadius(CornerRadius.TopLeft, 0, 0, CornerRadius.BottomLeft); } else if (i == ItemCount - 1) { cornerRadius = new CornerRadius(0, CornerRadius.TopRight, CornerRadius.BottomRight, 0); } _borderRenderHelper.Render(context, new Size(width, DesiredSize.Height), borderThickness, cornerRadius, BackgroundSizing.InnerBorderEdge, null, SelectedOptionBorderColor, new BoxShadows()); } } } } }