From 6f7db1c20cc761380f65a24c0184fb6c0888879f Mon Sep 17 00:00:00 2001 From: Alexander Prokhorov Date: Thu, 27 Nov 2025 17:42:42 +0300 Subject: [PATCH] Added IconRepeatButton, IconDropDownButton, IconSplitButton, IconToggleButton, IconToggleSplitButton (#834) * Added IconRepeatButton (#812) * Replaced control-specific PART_RootPanel workaround with AffectsArrange call fixing ReversibleStackPanel for the whole application. * Added IconToggleButton (#812) * Split IconRepeatButton into separate XAML file. * Added IconSplitButton (#812) * Added BindableClasses utility to allow propagating Classes property between controls. Avalonia currently doesn't support binding from Classes property, and binding to Classes property is heavily restricted. * Added IconToggleSplitButton (#812) * Fixed tab order in IconSplitButton and IconToggleSplitButton (DockPanel messes up tab order, TabIndex is global and makes it even worse, so just switched to Grid). * Added IconDropDownButton (#812) * Fixed IconPlacement inheritance. * Added redesigned IconButton demo section (#812) * Fixed spacing issues * Added redesigned demo sections for the newly added icon buttons (#812) * Replaced BindableClasses with ClassHelper. Fixed styling of default solid split icon buttons. (#812) * Replaced IIconButton with attached-like property getters and PseudolassesExtensions.Set(Classes); fixed arrow alignments in top/bottom split icon buttons (#812) * Applied fixes suggested by Copilot in code review (#812) * Fixed incorrect base type of IconDropDownButton (#812) * Fixed IconSplitButton and IconToggleSplitButton styles (#812) * Fixed secondary button color in checked state * Fixed applying of CornerRadius * Changed secondary button to square * Simplified template * Disabled demo of Colorful theme for IconSplitButton and IconToggleSplitButton --- demo/Ursa.Demo/Pages/IconButtonDemo.axaml | 719 +++++++++++++++--- .../Controls/IconButton.axaml | 340 +++++---- .../Controls/IconDropDownButton.axaml | 94 +++ .../Controls/IconRepeatButton.axaml | 19 + .../Controls/IconSplitButton.axaml | 81 ++ .../Controls/IconToggleButton.axaml | 236 ++++++ .../Controls/IconToggleSplitButton.axaml | 47 ++ src/Ursa.Themes.Semi/Controls/_index.axaml | 5 + src/Ursa/Common/ReversibleStackPanelUtils.cs | 21 + src/Ursa/Controls/Buttons/IconButton.cs | 75 +- .../Controls/Buttons/IconDropDownButton.cs | 57 ++ src/Ursa/Controls/Buttons/IconRepeatButton.cs | 57 ++ src/Ursa/Controls/Buttons/IconSplitButton.cs | 57 ++ src/Ursa/Controls/Buttons/IconToggleButton.cs | 56 ++ .../Controls/Buttons/IconToggleSplitButton.cs | 59 ++ 15 files changed, 1610 insertions(+), 313 deletions(-) create mode 100644 src/Ursa.Themes.Semi/Controls/IconDropDownButton.axaml create mode 100644 src/Ursa.Themes.Semi/Controls/IconRepeatButton.axaml create mode 100644 src/Ursa.Themes.Semi/Controls/IconSplitButton.axaml create mode 100644 src/Ursa.Themes.Semi/Controls/IconToggleButton.axaml create mode 100644 src/Ursa.Themes.Semi/Controls/IconToggleSplitButton.axaml create mode 100644 src/Ursa/Common/ReversibleStackPanelUtils.cs create mode 100644 src/Ursa/Controls/Buttons/IconDropDownButton.cs create mode 100644 src/Ursa/Controls/Buttons/IconRepeatButton.cs create mode 100644 src/Ursa/Controls/Buttons/IconSplitButton.cs create mode 100644 src/Ursa/Controls/Buttons/IconToggleButton.cs create mode 100644 src/Ursa/Controls/Buttons/IconToggleSplitButton.cs diff --git a/demo/Ursa.Demo/Pages/IconButtonDemo.axaml b/demo/Ursa.Demo/Pages/IconButtonDemo.axaml index f44c417..e55eaf5 100644 --- a/demo/Ursa.Demo/Pages/IconButtonDemo.axaml +++ b/demo/Ursa.Demo/Pages/IconButtonDemo.axaml @@ -8,60 +8,564 @@ xmlns:vm="clr-namespace:Ursa.Demo.ViewModels" xmlns:common="clr-namespace:Ursa.Common;assembly=Ursa" d:DesignHeight="NaN" - d:DesignWidth="800" + d:DesignWidth="1200" x:DataType="vm:IconButtonDemoViewModel" mc:Ignorable="d"> + + + + + + + - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -108,40 +612,54 @@ EnumType="common:Position" Value="{Binding SelectedPosition}" /> - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Ursa.Themes.Semi/Controls/IconButton.axaml b/src/Ursa.Themes.Semi/Controls/IconButton.axaml index b1c6287..de9e98b 100644 --- a/src/Ursa.Themes.Semi/Controls/IconButton.axaml +++ b/src/Ursa.Themes.Semi/Controls/IconButton.axaml @@ -4,91 +4,169 @@ xmlns:u="https://irihi.tech/ursa" xmlns:converters="using:Ursa.Themes.Semi.Converters"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + @@ -105,65 +183,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - + + + Classes="Solid" /> @@ -378,7 +395,6 @@ - - @@ -423,10 +438,8 @@ - + + + @@ -458,6 +472,7 @@ + @@ -467,10 +482,7 @@ - + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Ursa.Themes.Semi/Controls/IconDropDownButton.axaml b/src/Ursa.Themes.Semi/Controls/IconDropDownButton.axaml new file mode 100644 index 0000000..8e41eae --- /dev/null +++ b/src/Ursa.Themes.Semi/Controls/IconDropDownButton.axaml @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Ursa.Themes.Semi/Controls/IconRepeatButton.axaml b/src/Ursa.Themes.Semi/Controls/IconRepeatButton.axaml new file mode 100644 index 0000000..1b4f757 --- /dev/null +++ b/src/Ursa.Themes.Semi/Controls/IconRepeatButton.axaml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Ursa.Themes.Semi/Controls/IconSplitButton.axaml b/src/Ursa.Themes.Semi/Controls/IconSplitButton.axaml new file mode 100644 index 0000000..f542ba3 --- /dev/null +++ b/src/Ursa.Themes.Semi/Controls/IconSplitButton.axaml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Ursa.Themes.Semi/Controls/IconToggleButton.axaml b/src/Ursa.Themes.Semi/Controls/IconToggleButton.axaml new file mode 100644 index 0000000..3cb27aa --- /dev/null +++ b/src/Ursa.Themes.Semi/Controls/IconToggleButton.axaml @@ -0,0 +1,236 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Ursa.Themes.Semi/Controls/IconToggleSplitButton.axaml b/src/Ursa.Themes.Semi/Controls/IconToggleSplitButton.axaml new file mode 100644 index 0000000..c306ebf --- /dev/null +++ b/src/Ursa.Themes.Semi/Controls/IconToggleSplitButton.axaml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Ursa.Themes.Semi/Controls/_index.axaml b/src/Ursa.Themes.Semi/Controls/_index.axaml index 94fa975..4eea614 100644 --- a/src/Ursa.Themes.Semi/Controls/_index.axaml +++ b/src/Ursa.Themes.Semi/Controls/_index.axaml @@ -23,6 +23,11 @@ + + + + + diff --git a/src/Ursa/Common/ReversibleStackPanelUtils.cs b/src/Ursa/Common/ReversibleStackPanelUtils.cs new file mode 100644 index 0000000..78eaf98 --- /dev/null +++ b/src/Ursa/Common/ReversibleStackPanelUtils.cs @@ -0,0 +1,21 @@ +using Avalonia.Controls; +using Avalonia.Layout; + +namespace Ursa.Common; + +/// +/// Workaround for lacking call +/// on property. +/// Remove this workaround when the bug in Avalonia is fixed. +/// +internal class ReversibleStackPanelUtils : Layoutable +{ + private static int _isBugFixed = 0; + + public static void EnsureBugFixed() + { + if (Interlocked.CompareExchange(ref _isBugFixed, 1, 0) != 0) + return; + AffectsArrange(ReversibleStackPanel.ReverseOrderProperty); + } +} \ No newline at end of file diff --git a/src/Ursa/Controls/Buttons/IconButton.cs b/src/Ursa/Controls/Buttons/IconButton.cs index 36c2aeb..cdf665e 100644 --- a/src/Ursa/Controls/Buttons/IconButton.cs +++ b/src/Ursa/Controls/Buttons/IconButton.cs @@ -19,11 +19,12 @@ public class IconButton : Button public const string PC_EmptyContent = ":empty-content"; public const string PART_RootPanel = "PART_RootPanel"; - private Panel? _rootPanel; - public static readonly StyledProperty IconProperty = AvaloniaProperty.Register(nameof(Icon)); + public static object? GetIcon(ContentControl o) => o.GetValue(IconProperty); + public static void SetIcon(ContentControl o, object? value) => o.SetValue(IconProperty, value); + public object? Icon { get => GetValue(IconProperty); @@ -33,6 +34,9 @@ public class IconButton : Button public static readonly StyledProperty IconTemplateProperty = AvaloniaProperty.Register(nameof(IconTemplate)); + public static IDataTemplate? GetIconTemplate(ContentControl o) => o.GetValue(IconTemplateProperty); + public static void SetIconTemplate(ContentControl o, IDataTemplate? value) => o.SetValue(IconTemplateProperty, value); + public IDataTemplate? IconTemplate { get => GetValue(IconTemplateProperty); @@ -42,6 +46,9 @@ public class IconButton : Button public static readonly StyledProperty IsLoadingProperty = AvaloniaProperty.Register(nameof(IsLoading)); + public static bool GetIsLoading(ContentControl o) => o.GetValue(IsLoadingProperty); + public static void SetIsLoading(ContentControl o, bool value) => o.SetValue(IsLoadingProperty, value); + public bool IsLoading { get => GetValue(IsLoadingProperty); @@ -51,6 +58,9 @@ public class IconButton : Button public static readonly StyledProperty IconPlacementProperty = AvaloniaProperty.Register(nameof(IconPlacement), defaultValue: Position.Left); + public static Position GetIconPlacement(ContentControl o) => o.GetValue(IconPlacementProperty); + public static void SetIconPlacement(ContentControl o, Position value) => o.SetValue(IconPlacementProperty, value); + public Position IconPlacement { get => GetValue(IconPlacementProperty); @@ -59,49 +69,48 @@ public class IconButton : Button static IconButton() { - IconPlacementProperty.Changed.AddClassHandler((o, e) => + ReversibleStackPanelUtils.EnsureBugFixed(); + IconPlacementProperty.Changed.AddClassHandler((o, e) => { - o.SetPlacement(e.NewValue.Value, o.Icon); - o.InvalidateRootPanel(); + UpdateIconPseudoClasses(o, e.NewValue.Value, GetIcon(o)); }); - IconProperty.Changed.AddClassHandler((o, e) => + IconProperty.Changed.AddClassHandler((o, e) => { - o.SetPlacement(o.IconPlacement, e.NewValue.Value); + UpdateIconPseudoClasses(o, GetIconPlacement(o), e.NewValue.Value); + }); + ContentProperty.Changed.AddClassHandler((o, _) => + { + UpdateEmptyContentPseudoClass(o); }); - ContentProperty.Changed.AddClassHandler((o, e) => o.SetEmptyContent()); - } - - private void InvalidateRootPanel() => _rootPanel?.InvalidateArrange(); - - private void SetEmptyContent() - { - PseudoClasses.Set(PC_EmptyContent, Presenter?.Content is null); } protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); - _rootPanel = e.NameScope.Find(PART_RootPanel); - SetEmptyContent(); - SetPlacement(IconPlacement, Icon); + UpdateEmptyContentPseudoClass(this); + UpdateIconPseudoClasses(this, IconPlacement, Icon); } - private void SetPlacement(Position placement, object? icon) + internal static void UpdatePseudoClasses(ContentControl button) { - if (icon is null) - { - PseudoClasses.Set(PC_Empty, true); - PseudoClasses.Set(PC_Left, false); - PseudoClasses.Set(PC_Right, false); - PseudoClasses.Set(PC_Top, false); - PseudoClasses.Set(PC_Bottom, false); - return; - } + UpdateEmptyContentPseudoClass(button); + UpdateIconPseudoClasses(button, GetIconPlacement(button), GetIcon(button)); + } - PseudoClasses.Set(PC_Empty, false); - PseudoClasses.Set(PC_Left, placement == Position.Left); - PseudoClasses.Set(PC_Right, placement == Position.Right); - PseudoClasses.Set(PC_Top, placement == Position.Top); - PseudoClasses.Set(PC_Bottom, placement == Position.Bottom); + private static void UpdateEmptyContentPseudoClass(ContentControl button) + { + IPseudoClasses pseudo = button.Classes; + pseudo.Set(PC_EmptyContent, button.Content is null); + } + + private static void UpdateIconPseudoClasses(ContentControl button, Position placement, object? icon) + { + IPseudoClasses pseudo = button.Classes; + var hasIcon = icon is not null; + pseudo.Set(PC_Empty, !hasIcon); + pseudo.Set(PC_Left, hasIcon && placement == Position.Left); + pseudo.Set(PC_Right, hasIcon && placement == Position.Right); + pseudo.Set(PC_Top, hasIcon && placement == Position.Top); + pseudo.Set(PC_Bottom, hasIcon && placement == Position.Bottom); } } \ No newline at end of file diff --git a/src/Ursa/Controls/Buttons/IconDropDownButton.cs b/src/Ursa/Controls/Buttons/IconDropDownButton.cs new file mode 100644 index 0000000..7225a50 --- /dev/null +++ b/src/Ursa/Controls/Buttons/IconDropDownButton.cs @@ -0,0 +1,57 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Templates; +using Ursa.Common; + +namespace Ursa.Controls; + +public class IconDropDownButton : DropDownButton +{ + public static readonly StyledProperty IconProperty = + IconButton.IconProperty.AddOwner(); + + public object? Icon + { + get => GetValue(IconProperty); + set => SetValue(IconProperty, value); + } + + public static readonly StyledProperty IconTemplateProperty = + IconButton.IconTemplateProperty.AddOwner(); + + public IDataTemplate? IconTemplate + { + get => GetValue(IconTemplateProperty); + set => SetValue(IconTemplateProperty, value); + } + + public static readonly StyledProperty IsLoadingProperty = + IconButton.IsLoadingProperty.AddOwner(); + + public bool IsLoading + { + get => GetValue(IsLoadingProperty); + set => SetValue(IsLoadingProperty, value); + } + + public static readonly StyledProperty IconPlacementProperty = + IconButton.IconPlacementProperty.AddOwner(); + + public Position IconPlacement + { + get => GetValue(IconPlacementProperty); + set => SetValue(IconPlacementProperty, value); + } + + static IconDropDownButton() + { + ReversibleStackPanelUtils.EnsureBugFixed(); + } + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + IconButton.UpdatePseudoClasses(this); + } +} \ No newline at end of file diff --git a/src/Ursa/Controls/Buttons/IconRepeatButton.cs b/src/Ursa/Controls/Buttons/IconRepeatButton.cs new file mode 100644 index 0000000..f48c0a6 --- /dev/null +++ b/src/Ursa/Controls/Buttons/IconRepeatButton.cs @@ -0,0 +1,57 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Templates; +using Ursa.Common; + +namespace Ursa.Controls; + +public class IconRepeatButton : RepeatButton +{ + public static readonly StyledProperty IconProperty = + IconButton.IconProperty.AddOwner(); + + public object? Icon + { + get => GetValue(IconProperty); + set => SetValue(IconProperty, value); + } + + public static readonly StyledProperty IconTemplateProperty = + IconButton.IconTemplateProperty.AddOwner(); + + public IDataTemplate? IconTemplate + { + get => GetValue(IconTemplateProperty); + set => SetValue(IconTemplateProperty, value); + } + + public static readonly StyledProperty IsLoadingProperty = + IconButton.IsLoadingProperty.AddOwner(); + + public bool IsLoading + { + get => GetValue(IsLoadingProperty); + set => SetValue(IsLoadingProperty, value); + } + + public static readonly StyledProperty IconPlacementProperty = + IconButton.IconPlacementProperty.AddOwner(); + + public Position IconPlacement + { + get => GetValue(IconPlacementProperty); + set => SetValue(IconPlacementProperty, value); + } + + static IconRepeatButton() + { + ReversibleStackPanelUtils.EnsureBugFixed(); + } + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + IconButton.UpdatePseudoClasses(this); + } +} \ No newline at end of file diff --git a/src/Ursa/Controls/Buttons/IconSplitButton.cs b/src/Ursa/Controls/Buttons/IconSplitButton.cs new file mode 100644 index 0000000..db36c76 --- /dev/null +++ b/src/Ursa/Controls/Buttons/IconSplitButton.cs @@ -0,0 +1,57 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Templates; +using Ursa.Common; + +namespace Ursa.Controls; + +public class IconSplitButton : SplitButton +{ + public static readonly StyledProperty IconProperty = + IconButton.IconProperty.AddOwner(); + + public object? Icon + { + get => GetValue(IconProperty); + set => SetValue(IconProperty, value); + } + + public static readonly StyledProperty IconTemplateProperty = + IconButton.IconTemplateProperty.AddOwner(); + + public IDataTemplate? IconTemplate + { + get => GetValue(IconTemplateProperty); + set => SetValue(IconTemplateProperty, value); + } + + public static readonly StyledProperty IsLoadingProperty = + IconButton.IsLoadingProperty.AddOwner(); + + public bool IsLoading + { + get => GetValue(IsLoadingProperty); + set => SetValue(IsLoadingProperty, value); + } + + public static readonly StyledProperty IconPlacementProperty = + IconButton.IconPlacementProperty.AddOwner(); + + public Position IconPlacement + { + get => GetValue(IconPlacementProperty); + set => SetValue(IconPlacementProperty, value); + } + + static IconSplitButton() + { + ReversibleStackPanelUtils.EnsureBugFixed(); + } + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + IconButton.UpdatePseudoClasses(this); + } +} \ No newline at end of file diff --git a/src/Ursa/Controls/Buttons/IconToggleButton.cs b/src/Ursa/Controls/Buttons/IconToggleButton.cs new file mode 100644 index 0000000..7480e4d --- /dev/null +++ b/src/Ursa/Controls/Buttons/IconToggleButton.cs @@ -0,0 +1,56 @@ +using Avalonia; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Templates; +using Ursa.Common; + +namespace Ursa.Controls; + +public class IconToggleButton : ToggleButton +{ + public static readonly StyledProperty IconProperty = + IconButton.IconProperty.AddOwner(); + + public object? Icon + { + get => GetValue(IconProperty); + set => SetValue(IconProperty, value); + } + + public static readonly StyledProperty IconTemplateProperty = + IconButton.IconTemplateProperty.AddOwner(); + + public IDataTemplate? IconTemplate + { + get => GetValue(IconTemplateProperty); + set => SetValue(IconTemplateProperty, value); + } + + public static readonly StyledProperty IsLoadingProperty = + IconButton.IsLoadingProperty.AddOwner(); + + public bool IsLoading + { + get => GetValue(IsLoadingProperty); + set => SetValue(IsLoadingProperty, value); + } + + public static readonly StyledProperty IconPlacementProperty = + IconButton.IconPlacementProperty.AddOwner(); + + public Position IconPlacement + { + get => GetValue(IconPlacementProperty); + set => SetValue(IconPlacementProperty, value); + } + + static IconToggleButton() + { + ReversibleStackPanelUtils.EnsureBugFixed(); + } + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + IconButton.UpdatePseudoClasses(this); + } +} \ No newline at end of file diff --git a/src/Ursa/Controls/Buttons/IconToggleSplitButton.cs b/src/Ursa/Controls/Buttons/IconToggleSplitButton.cs new file mode 100644 index 0000000..9f78aa3 --- /dev/null +++ b/src/Ursa/Controls/Buttons/IconToggleSplitButton.cs @@ -0,0 +1,59 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Templates; +using Ursa.Common; + +namespace Ursa.Controls; + +public class IconToggleSplitButton : ToggleSplitButton +{ + public static readonly StyledProperty IconProperty = + IconButton.IconProperty.AddOwner(); + + public object? Icon + { + get => GetValue(IconProperty); + set => SetValue(IconProperty, value); + } + + public static readonly StyledProperty IconTemplateProperty = + IconButton.IconTemplateProperty.AddOwner(); + + public IDataTemplate? IconTemplate + { + get => GetValue(IconTemplateProperty); + set => SetValue(IconTemplateProperty, value); + } + + public static readonly StyledProperty IsLoadingProperty = + IconButton.IsLoadingProperty.AddOwner(); + + public bool IsLoading + { + get => GetValue(IsLoadingProperty); + set => SetValue(IsLoadingProperty, value); + } + + public static readonly StyledProperty IconPlacementProperty = + IconButton.IconPlacementProperty.AddOwner(); + + public Position IconPlacement + { + get => GetValue(IconPlacementProperty); + set => SetValue(IconPlacementProperty, value); + } + + static IconToggleSplitButton() + { + ReversibleStackPanelUtils.EnsureBugFixed(); + } + + protected override Type StyleKeyOverride => GetType(); + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + IconButton.UpdatePseudoClasses(this); + } +} \ No newline at end of file