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
This commit is contained in:
Alexander Prokhorov
2025-11-27 17:42:42 +03:00
committed by GitHub
parent a4906d5130
commit 6f7db1c20c
15 changed files with 1610 additions and 313 deletions

View File

@@ -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<object?> IconProperty =
AvaloniaProperty.Register<IconButton, object?>(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<IDataTemplate?> IconTemplateProperty =
AvaloniaProperty.Register<IconButton, IDataTemplate?>(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<bool> IsLoadingProperty =
AvaloniaProperty.Register<IconButton, bool>(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<Position> IconPlacementProperty =
AvaloniaProperty.Register<IconButton, Position>(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<IconButton, Position>((o, e) =>
ReversibleStackPanelUtils.EnsureBugFixed();
IconPlacementProperty.Changed.AddClassHandler<ContentControl, Position>((o, e) =>
{
o.SetPlacement(e.NewValue.Value, o.Icon);
o.InvalidateRootPanel();
UpdateIconPseudoClasses(o, e.NewValue.Value, GetIcon(o));
});
IconProperty.Changed.AddClassHandler<IconButton, object?>((o, e) =>
IconProperty.Changed.AddClassHandler<ContentControl, object?>((o, e) =>
{
o.SetPlacement(o.IconPlacement, e.NewValue.Value);
UpdateIconPseudoClasses(o, GetIconPlacement(o), e.NewValue.Value);
});
ContentProperty.Changed.AddClassHandler<ContentControl, object?>((o, _) =>
{
UpdateEmptyContentPseudoClass(o);
});
ContentProperty.Changed.AddClassHandler<IconButton>((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<Panel>(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);
}
}

View File

@@ -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<object?> IconProperty =
IconButton.IconProperty.AddOwner<IconDropDownButton>();
public object? Icon
{
get => GetValue(IconProperty);
set => SetValue(IconProperty, value);
}
public static readonly StyledProperty<IDataTemplate?> IconTemplateProperty =
IconButton.IconTemplateProperty.AddOwner<IconDropDownButton>();
public IDataTemplate? IconTemplate
{
get => GetValue(IconTemplateProperty);
set => SetValue(IconTemplateProperty, value);
}
public static readonly StyledProperty<bool> IsLoadingProperty =
IconButton.IsLoadingProperty.AddOwner<IconDropDownButton>();
public bool IsLoading
{
get => GetValue(IsLoadingProperty);
set => SetValue(IsLoadingProperty, value);
}
public static readonly StyledProperty<Position> IconPlacementProperty =
IconButton.IconPlacementProperty.AddOwner<IconDropDownButton>();
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);
}
}

View File

@@ -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<object?> IconProperty =
IconButton.IconProperty.AddOwner<IconRepeatButton>();
public object? Icon
{
get => GetValue(IconProperty);
set => SetValue(IconProperty, value);
}
public static readonly StyledProperty<IDataTemplate?> IconTemplateProperty =
IconButton.IconTemplateProperty.AddOwner<IconRepeatButton>();
public IDataTemplate? IconTemplate
{
get => GetValue(IconTemplateProperty);
set => SetValue(IconTemplateProperty, value);
}
public static readonly StyledProperty<bool> IsLoadingProperty =
IconButton.IsLoadingProperty.AddOwner<IconRepeatButton>();
public bool IsLoading
{
get => GetValue(IsLoadingProperty);
set => SetValue(IsLoadingProperty, value);
}
public static readonly StyledProperty<Position> IconPlacementProperty =
IconButton.IconPlacementProperty.AddOwner<IconRepeatButton>();
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);
}
}

View File

@@ -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<object?> IconProperty =
IconButton.IconProperty.AddOwner<IconSplitButton>();
public object? Icon
{
get => GetValue(IconProperty);
set => SetValue(IconProperty, value);
}
public static readonly StyledProperty<IDataTemplate?> IconTemplateProperty =
IconButton.IconTemplateProperty.AddOwner<IconSplitButton>();
public IDataTemplate? IconTemplate
{
get => GetValue(IconTemplateProperty);
set => SetValue(IconTemplateProperty, value);
}
public static readonly StyledProperty<bool> IsLoadingProperty =
IconButton.IsLoadingProperty.AddOwner<IconSplitButton>();
public bool IsLoading
{
get => GetValue(IsLoadingProperty);
set => SetValue(IsLoadingProperty, value);
}
public static readonly StyledProperty<Position> IconPlacementProperty =
IconButton.IconPlacementProperty.AddOwner<IconSplitButton>();
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);
}
}

View File

@@ -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<object?> IconProperty =
IconButton.IconProperty.AddOwner<IconToggleButton>();
public object? Icon
{
get => GetValue(IconProperty);
set => SetValue(IconProperty, value);
}
public static readonly StyledProperty<IDataTemplate?> IconTemplateProperty =
IconButton.IconTemplateProperty.AddOwner<IconToggleButton>();
public IDataTemplate? IconTemplate
{
get => GetValue(IconTemplateProperty);
set => SetValue(IconTemplateProperty, value);
}
public static readonly StyledProperty<bool> IsLoadingProperty =
IconButton.IsLoadingProperty.AddOwner<IconToggleButton>();
public bool IsLoading
{
get => GetValue(IsLoadingProperty);
set => SetValue(IsLoadingProperty, value);
}
public static readonly StyledProperty<Position> IconPlacementProperty =
IconButton.IconPlacementProperty.AddOwner<IconToggleButton>();
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);
}
}

View File

@@ -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<object?> IconProperty =
IconButton.IconProperty.AddOwner<IconToggleSplitButton>();
public object? Icon
{
get => GetValue(IconProperty);
set => SetValue(IconProperty, value);
}
public static readonly StyledProperty<IDataTemplate?> IconTemplateProperty =
IconButton.IconTemplateProperty.AddOwner<IconToggleSplitButton>();
public IDataTemplate? IconTemplate
{
get => GetValue(IconTemplateProperty);
set => SetValue(IconTemplateProperty, value);
}
public static readonly StyledProperty<bool> IsLoadingProperty =
IconButton.IsLoadingProperty.AddOwner<IconToggleSplitButton>();
public bool IsLoading
{
get => GetValue(IsLoadingProperty);
set => SetValue(IsLoadingProperty, value);
}
public static readonly StyledProperty<Position> IconPlacementProperty =
IconButton.IconPlacementProperty.AddOwner<IconToggleSplitButton>();
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);
}
}