feat: Add Tag styles and demos.

This commit is contained in:
NaBian committed 2026-06-16 23:50:41 +08:00
1 parent 5bfbbe1904
commit 4cd78a4b3f
7 files changed
+499 -1

No files matched your search

@@ -54,7 +54,8 @@
[ "UniformSpacingPanel", "UniformSpacingPanelDemo", "Brush.IncreaseHorizontalSpacing", "", "Layout" ],
[ "ProgressBar", "ProgressBarDemo", "Brush.ProgressBar", "", "Feedback" ],
[ "Rate", "RateDemo", "Brush.Favorite", "", "Feedback" ],
[ "Loading", "LoadingDemo", "Brush.Loading", "", "Feedback" ]
[ "Loading", "LoadingDemo", "Brush.Loading", "", "Feedback" ],
[ "Tag", "TagDemo", "Brush.Tag", "", "DataDisplay" ]
]
}
]
@@ -0,0 +1,79 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:hc="https://handyorg.github.io/handycontrol"
x:Class="HandyControlDemo.UserControl.TagDemo"
Background="{DynamicResource RegionBrush}">
<UserControl.Resources>
<ControlTheme x:Key="TagCustomStyle"
BasedOn="{StaticResource {x:Type hc:Tag}}"
TargetType="hc:Tag">
<Setter Property="Content"
Value="{Binding Name}" />
<Setter Property="Margin"
Value="5" />
<Setter Property="IsSelected"
Value="{Binding IsSelected}" />
</ControlTheme>
</UserControl.Resources>
<ScrollViewer>
<StackPanel Margin="11">
<hc:TagContainer Margin="11"
MaxWidth="420"
Padding="5">
<hc:Tag Header="H"
hc:TitleElement.Background="{DynamicResource PrimaryBrush}"
Margin="5"
ShowCloseButton="False"
Content="TextText" />
<hc:Tag Margin="5"
Selectable="True"
Content="TextTextText" />
<hc:Tag Margin="5"
ShowCloseButton="False"
Content="TextTextTextText" />
<hc:Tag Margin="5"
Content="TextTextTextTextText" />
<hc:Tag Margin="5"
IsSelected="True"
Selectable="True"
ShowCloseButton="False"
Content="TextTextTextText" />
<hc:Tag Margin="5"
Content="TextTextText" />
<hc:Tag Margin="5"
ShowCloseButton="False"
Content="TextText">
<hc:Tag.Header>
<Image Source="/Resources/Img/Album/1.jpg" />
</hc:Tag.Header>
</hc:Tag>
<hc:Tag Margin="5"
IsSelected="True"
Selectable="True"
Content="TextTextText">
<hc:Tag.Header>
<Image Source="/Resources/Img/Avatar/avatar2.png" />
</hc:Tag.Header>
</hc:Tag>
<hc:Tag Margin="5"
ShowCloseButton="False"
Content="TextTextTextText" />
<hc:Tag Margin="5"
Content="TextTextTextTextText" />
</hc:TagContainer>
<hc:TagContainer Name="BoundContainer"
Margin="11"
MaxWidth="420"
Padding="5"
ItemContainerTheme="{StaticResource TagCustomStyle}" />
<DockPanel LastChildFill="True"
Margin="11,0,11,11">
<Button Name="AddButton"
Margin="10,0,0,0"
DockPanel.Dock="Right"
Content="Add an item" />
<TextBox Name="TagNameBox" />
</DockPanel>
</StackPanel>
</ScrollViewer>
</UserControl>
@@ -0,0 +1,63 @@
using System.Collections.ObjectModel;
using Avalonia.Interactivity;
namespace HandyControlDemo.UserControl;
public class TagDemoItem : Avalonia.AvaloniaObject
{
public static readonly Avalonia.StyledProperty<string?> NameProperty =
Avalonia.AvaloniaProperty.Register<TagDemoItem, string?>(nameof(Name));
public static readonly Avalonia.StyledProperty<bool> IsSelectedProperty =
Avalonia.AvaloniaProperty.Register<TagDemoItem, bool>(nameof(IsSelected));
public string? Name
{
get => GetValue(NameProperty);
set => SetValue(NameProperty, value);
}
public bool IsSelected
{
get => GetValue(IsSelectedProperty);
set => SetValue(IsSelectedProperty, value);
}
}
public partial class TagDemo : Avalonia.Controls.UserControl
{
public ObservableCollection<TagDemoItem> DataList { get; } = new();
public TagDemo()
{
InitializeComponent();
for (var i = 1; i <= 10; i++)
{
DataList.Add(new TagDemoItem
{
Name = $"Name{i}",
IsSelected = i % 2 == 0
});
}
BoundContainer.ItemsSource = DataList;
AddButton.Click += AddButton_Click;
}
private void AddButton_Click(object? sender, RoutedEventArgs e)
{
var name = TagNameBox.Text;
if (string.IsNullOrWhiteSpace(name))
{
return;
}
DataList.Insert(0, new TagDemoItem
{
Name = name,
IsSelected = DataList.Count % 2 == 0
});
TagNameBox.Text = string.Empty;
}
}
@@ -0,0 +1,131 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Interactivity;
namespace HandyControl.Controls;
[TemplatePart(PartCloseButton, typeof(Button))]
public class Tag : HeaderedContentControl
{
public const string PartCloseButton = "PART_CloseButton";
private Button? _closeButton;
public static readonly StyledProperty<bool> ShowCloseButtonProperty =
AvaloniaProperty.Register<Tag, bool>(nameof(ShowCloseButton), true);
public bool ShowCloseButton
{
get => GetValue(ShowCloseButtonProperty);
set => SetValue(ShowCloseButtonProperty, value);
}
public static readonly StyledProperty<bool> SelectableProperty =
AvaloniaProperty.Register<Tag, bool>(nameof(Selectable));
public bool Selectable
{
get => GetValue(SelectableProperty);
set => SetValue(SelectableProperty, value);
}
public static readonly StyledProperty<bool> IsSelectedProperty =
AvaloniaProperty.Register<Tag, bool>(nameof(IsSelected));
public bool IsSelected
{
get => GetValue(IsSelectedProperty);
set => SetValue(IsSelectedProperty, value);
}
public static readonly RoutedEvent<RoutedEventArgs> SelectedEvent =
RoutedEvent.Register<Tag, RoutedEventArgs>(nameof(Selected), RoutingStrategies.Bubble);
public event System.EventHandler<RoutedEventArgs> Selected
{
add => AddHandler(SelectedEvent, value);
remove => RemoveHandler(SelectedEvent, value);
}
public static readonly RoutedEvent<CancelRoutedEventArgs> ClosingEvent =
RoutedEvent.Register<Tag, CancelRoutedEventArgs>(nameof(Closing), RoutingStrategies.Bubble);
public event System.EventHandler<CancelRoutedEventArgs> Closing
{
add => AddHandler(ClosingEvent, value);
remove => RemoveHandler(ClosingEvent, value);
}
public static readonly RoutedEvent<RoutedEventArgs> ClosedEvent =
RoutedEvent.Register<Tag, RoutedEventArgs>(nameof(Closed), RoutingStrategies.Bubble);
public event System.EventHandler<RoutedEventArgs> Closed
{
add => AddHandler(ClosedEvent, value);
remove => RemoveHandler(ClosedEvent, value);
}
static Tag()
{
IsSelectedProperty.Changed.AddClassHandler<Tag>((tag, e) => tag.OnIsSelectedChanged(e));
}
private void OnIsSelectedChanged(AvaloniaPropertyChangedEventArgs e)
{
var newValue = e.GetNewValue<bool>();
PseudoClasses.Set(":selected", newValue);
RaiseEvent(new RoutedEventArgs(SelectedEvent, this));
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
if (_closeButton != null)
{
_closeButton.Click -= CloseButton_Click;
}
_closeButton = e.NameScope.Find<Button>(PartCloseButton);
if (_closeButton != null)
{
_closeButton.Click += CloseButton_Click;
}
}
private void CloseButton_Click(object? sender, RoutedEventArgs e)
{
RequestClose();
}
internal void RequestClose()
{
var argsClosing = new CancelRoutedEventArgs(ClosingEvent, this);
RaiseEvent(argsClosing);
if (argsClosing.Cancel)
{
return;
}
RaiseEvent(new RoutedEventArgs(ClosedEvent, this));
}
protected override void OnPointerPressed(PointerPressedEventArgs e)
{
base.OnPointerPressed(e);
if (Selectable && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{
IsSelected = true;
}
}
public void Hide()
{
IsVisible = false;
}
}
@@ -0,0 +1,76 @@
using System.Collections;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
namespace HandyControl.Controls;
public class TagContainer : ItemsControl
{
public static readonly AttachedProperty<bool> ShowCloseButtonProperty =
AvaloniaProperty.RegisterAttached<TagContainer, AvaloniaObject, bool>(
"ShowCloseButton", defaultValue: true, inherits: true);
public static void SetShowCloseButton(AvaloniaObject element, bool value) =>
element.SetValue(ShowCloseButtonProperty, value);
public static bool GetShowCloseButton(AvaloniaObject element) =>
element.GetValue(ShowCloseButtonProperty);
public TagContainer()
{
AddHandler(HandyControl.Controls.Tag.ClosedEvent, Tag_OnClosed);
}
private void Tag_OnClosed(object? sender, RoutedEventArgs e)
{
if (e.Source is not Tag tag)
{
return;
}
tag.Hide();
if (ItemsSource == null)
{
Items.Remove(tag);
}
else if (ItemsSource is IList list)
{
var item = ItemFromContainer(tag);
if (item != null && list.Contains(item))
{
list.Remove(item);
}
}
}
protected override bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
{
if (item is Tag)
{
recycleKey = null;
return false;
}
recycleKey = typeof(Tag);
return true;
}
protected override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey) => new Tag();
protected override void PrepareContainerForItemOverride(Control container, object? item, int index)
{
base.PrepareContainerForItemOverride(container, item, index);
// ItemsControl auto-binds HeaderedContentControl.Header to the item.
// Tag is a HeaderedContentControl but, in TagContainer, the Header is meant
// for an explicit avatar/icon. Clear the auto-assigned Header to prevent the
// item's ToString() bleeding into the header circle. Skip when the item IS
// the container (Tag declared directly in XAML) so an explicit Header is kept.
if (container is Tag tag && !ReferenceEquals(container, item))
{
tag.ClearValue(Avalonia.Controls.Primitives.HeaderedContentControl.HeaderProperty);
}
}
}
@@ -70,6 +70,7 @@
<MergeResourceInclude Source="avares://HandyControl/Themes/Styles/ElementGroup.axaml" />
<MergeResourceInclude Source="avares://HandyControl/Themes/Styles/ButtonGroup.axaml" />
<MergeResourceInclude Source="avares://HandyControl/Themes/Styles/Gravatar.axaml" />
<MergeResourceInclude Source="avares://HandyControl/Themes/Styles/Tag.axaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Styles.Resources>
@@ -0,0 +1,147 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:ClassModifier="internal"
xmlns:hc="clr-namespace:HandyControl.Controls"
xmlns:sys="using:System">
<ControlTheme x:Key="TagBaseStyle"
TargetType="hc:Tag">
<Setter Property="Background"
Value="{DynamicResource SecondaryRegionBrush}" />
<Setter Property="BorderBrush"
Value="{DynamicResource BorderBrush}" />
<Setter Property="Padding"
Value="{StaticResource DefaultControlPadding}" />
<Setter Property="HorizontalContentAlignment"
Value="Center" />
<Setter Property="VerticalContentAlignment"
Value="Center" />
<Setter Property="HorizontalAlignment"
Value="Center" />
<Setter Property="VerticalAlignment"
Value="Center" />
<Setter Property="Height"
Value="{StaticResource DefaultControlHeight}" />
<Setter Property="hc:TitleElement.Foreground"
Value="{DynamicResource TextIconBrush}" />
<Setter Property="ShowCloseButton"
Value="{Binding Path=(hc:TagContainer.ShowCloseButton), RelativeSource={RelativeSource Self}}" />
<Setter Property="Template">
<ControlTemplate TargetType="hc:Tag">
<Border Theme="{StaticResource BorderCircular}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Background="{TemplateBinding Background}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Border IsVisible="{TemplateBinding Header, Converter={x:Static ObjectConverters.IsNotNull}}"
Width="{Binding $self.Bounds.Height}"
Theme="{StaticResource BorderCircularClip}"
BorderBrush="{Binding Path=(hc:TitleElement.BorderBrush), RelativeSource={RelativeSource TemplatedParent}}"
Background="{Binding Path=(hc:TitleElement.Background), RelativeSource={RelativeSource TemplatedParent}}">
<ContentPresenter TextElement.Foreground="{Binding Path=(hc:TitleElement.Foreground), RelativeSource={RelativeSource TemplatedParent}}"
Content="{TemplateBinding Header}"
ContentTemplate="{TemplateBinding HeaderTemplate}"
VerticalAlignment="Center"
HorizontalAlignment="Center">
<ContentPresenter.DataTemplates>
<DataTemplate DataType="sys:String">
<TextBlock Text="{Binding}"
VerticalAlignment="Center"
HorizontalAlignment="Center" />
</DataTemplate>
</ContentPresenter.DataTemplates>
</ContentPresenter>
</Border>
<ContentPresenter Margin="{TemplateBinding Padding}"
Grid.Column="1"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
RecognizesAccessKey="True" />
<Button Name="PART_CloseButton"
Focusable="False"
Padding="0"
BorderThickness="0"
hc:IconElement.Width="8"
IsVisible="{TemplateBinding ShowCloseButton}"
Margin="0,0,6,0"
Width="16"
Grid.Column="2"
Theme="{StaticResource ButtonIcon}"
hc:IconElement.Geometry="{StaticResource DeleteGeometry}"
Foreground="{TemplateBinding Foreground}" />
</Grid>
</Border>
</ControlTemplate>
</Setter>
<Style Selector="^/template/ Button#PART_CloseButton:pointerover">
<Setter Property="hc:IconElement.Geometry"
Value="{StaticResource DeleteFillCircleGeometry}" />
<Setter Property="hc:IconElement.Width"
Value="14" />
</Style>
<Style Selector="^:disabled">
<Setter Property="Opacity"
Value="0.4" />
</Style>
<Style Selector="^:pointerover">
<Setter Property="Background"
Value="{DynamicResource DarkMaskBrush}" />
</Style>
<Style Selector="^:selected">
<Setter Property="Background"
Value="{DynamicResource PrimaryBrush}" />
<Setter Property="Foreground"
Value="{DynamicResource TextIconBrush}" />
</Style>
</ControlTheme>
<ControlTheme x:Key="{x:Type hc:Tag}"
BasedOn="{StaticResource TagBaseStyle}"
TargetType="hc:Tag" />
<ControlTheme x:Key="TagContainerBaseStyle"
TargetType="hc:TagContainer">
<Setter Property="hc:BorderElement.CornerRadius"
Value="{StaticResource DefaultCornerRadius}" />
<Setter Property="ItemContainerTheme"
Value="{StaticResource TagBaseStyle}" />
<Setter Property="BorderThickness"
Value="1" />
<Setter Property="BorderBrush"
Value="{DynamicResource BorderBrush}" />
<Setter Property="ItemsPanel">
<ItemsPanelTemplate>
<WrapPanel />
</ItemsPanelTemplate>
</Setter>
<Setter Property="Template">
<ControlTemplate TargetType="hc:TagContainer">
<Border CornerRadius="{Binding Path=(hc:BorderElement.CornerRadius), RelativeSource={RelativeSource TemplatedParent}}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Background="{TemplateBinding Background}">
<ScrollViewer Focusable="False">
<ItemsPresenter Margin="{TemplateBinding Padding}"
ItemsPanel="{TemplateBinding ItemsPanel}" />
</ScrollViewer>
</Border>
</ControlTemplate>
</Setter>
</ControlTheme>
<ControlTheme x:Key="{x:Type hc:TagContainer}"
BasedOn="{StaticResource TagContainerBaseStyle}"
TargetType="hc:TagContainer" />
</ResourceDictionary>