diff --git a/src/AtomUI.Controls/AtomUIOSSControlThemesProvider.axaml b/src/AtomUI.Controls/AtomUIOSSControlThemesProvider.axaml
index c185c532c..bcc56ca4f 100644
--- a/src/AtomUI.Controls/AtomUIOSSControlThemesProvider.axaml
+++ b/src/AtomUI.Controls/AtomUIOSSControlThemesProvider.axaml
@@ -8,6 +8,7 @@
+
diff --git a/src/AtomUI.Controls/Avatar/Avatar.cs b/src/AtomUI.Controls/Avatar/Avatar.cs
new file mode 100644
index 000000000..128d70610
--- /dev/null
+++ b/src/AtomUI.Controls/Avatar/Avatar.cs
@@ -0,0 +1,311 @@
+using System.Diagnostics;
+using AtomUI.Controls.Themes;
+using AtomUI.IconPkg;
+using AtomUI.Media;
+using AtomUI.Theme;
+using AtomUI.Theme.Utils;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.Primitives;
+using Avalonia.Data;
+using Avalonia.Media;
+using Avalonia.Media.Transformation;
+using Avalonia.Metadata;
+
+namespace AtomUI.Controls;
+
+public enum AvatarSizeType
+{
+ Large,
+ Middle,
+ Small,
+ Custom
+}
+
+public enum AvatarShape
+{
+ Circle,
+ Square
+}
+
+internal enum AvatarContentType
+{
+ Icon,
+ BitmapImage,
+ SvgImage,
+ Text
+}
+
+public class Avatar : TemplatedControl, IControlSharedTokenResourcesHost, IMotionAwareControl
+{
+ #region 公共属性定义
+
+ public static readonly StyledProperty GapProperty =
+ AvaloniaProperty.Register(nameof(Gap), defaultValue: 4.0);
+
+ public static readonly StyledProperty IconProperty =
+ AvaloniaProperty.Register(nameof(Icon));
+
+ public static readonly StyledProperty BitmapSrcProperty =
+ AvaloniaProperty.Register(nameof(BitmapSrc));
+
+ public static readonly StyledProperty SrcProperty =
+ AvaloniaProperty.Register(nameof(Src));
+
+ public static readonly StyledProperty TextProperty =
+ AvaloniaProperty.Register(nameof (Text));
+
+ public static readonly StyledProperty SizeTypeProperty =
+ AvaloniaProperty.Register(nameof(SizeType),
+ AvatarSizeType.Middle);
+
+ public static readonly StyledProperty SizeProperty =
+ AvaloniaProperty.Register(nameof(Size), Double.NaN);
+
+ public static readonly StyledProperty ShapeProperty =
+ AvaloniaProperty.Register(nameof(Shape), AvatarShape.Circle);
+
+ public static readonly StyledProperty IsMotionEnabledProperty =
+ MotionAwareControlProperty.IsMotionEnabledProperty.AddOwner();
+
+ public double Gap
+ {
+ get => GetValue(GapProperty);
+ set => SetValue(GapProperty, value);
+ }
+
+ public Icon? Icon
+ {
+ get => GetValue(IconProperty);
+ set => SetValue(IconProperty, value);
+ }
+
+ public IImage? BitmapSrc
+ {
+ get => GetValue(BitmapSrcProperty);
+ set => SetValue(BitmapSrcProperty, value);
+ }
+
+ public string? Src
+ {
+ get => GetValue(SrcProperty);
+ set => SetValue(SrcProperty, value);
+ }
+
+ [Content]
+ public string? Text
+ {
+ get => GetValue(TextProperty);
+ set => SetValue(TextProperty, value);
+ }
+
+ public AvatarSizeType SizeType
+ {
+ get => GetValue(SizeTypeProperty);
+ set => SetValue(SizeTypeProperty, value);
+ }
+
+ public double Size
+ {
+ get => GetValue(SizeProperty);
+ set => SetValue(SizeProperty, value);
+ }
+
+ public AvatarShape Shape
+ {
+ get => GetValue(ShapeProperty);
+ set => SetValue(ShapeProperty, value);
+ }
+
+ public bool IsMotionEnabled
+ {
+ get => GetValue(IsMotionEnabledProperty);
+ set => SetValue(IsMotionEnabledProperty, value);
+ }
+
+ #endregion
+
+ #region 内部属性定义
+ internal static readonly StyledProperty EffectiveIconSizeProperty =
+ AvaloniaProperty.Register(nameof(EffectiveIconSize));
+
+ internal static readonly StyledProperty TextRenderTransformProperty =
+ AvaloniaProperty.Register(nameof(TextRenderTransform));
+
+ internal static readonly DirectProperty ContentTypeProperty =
+ AvaloniaProperty.RegisterDirect(
+ nameof(ContentType),
+ o => o.ContentType,
+ (o, v) => o.ContentType = v);
+
+ internal double EffectiveIconSize
+ {
+ get => GetValue(EffectiveIconSizeProperty);
+ set => SetValue(EffectiveIconSizeProperty, value);
+ }
+
+ public ITransform? TextRenderTransform
+ {
+ get => GetValue(TextRenderTransformProperty);
+ set => SetValue(TextRenderTransformProperty, value);
+ }
+
+ private AvatarContentType _contentType = AvatarContentType.Icon;
+
+ internal AvatarContentType ContentType
+ {
+ get => _contentType;
+ set => SetAndRaise(ContentTypeProperty, ref _contentType, value);
+ }
+
+ Control IMotionAwareControl.PropertyBindTarget => this;
+ Control IControlSharedTokenResourcesHost.HostControl => this;
+ string IControlSharedTokenResourcesHost.TokenId => ButtonToken.ID;
+
+ #endregion
+
+ private AvatarSizeType? _originSizeType;
+ private TextBlock? _textPresenter;
+
+ static Avatar()
+ {
+ AffectsMeasure(SizeTypeProperty, TextProperty);
+ AffectsRender(ShapeProperty, IconProperty, SrcProperty, GapProperty);
+ }
+
+ public Avatar()
+ {
+ this.RegisterResources();
+ this.BindMotionProperties();
+ }
+
+ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
+ {
+ base.OnPropertyChanged(change);
+ if (change.Property == SizeProperty)
+ {
+ if (!double.IsNaN(Size))
+ {
+ _originSizeType = SizeType;
+ SizeType = AvatarSizeType.Custom;
+ }
+ else
+ {
+ if (_originSizeType.HasValue)
+ {
+ SizeType = _originSizeType.Value;
+ }
+ }
+ }
+ else if (change.Property == SizeTypeProperty)
+ {
+ ConfigureIconSize();
+ }
+ else if (change.Property == SrcProperty ||
+ change.Property == IconProperty ||
+ change.Property == TextProperty)
+ {
+ ConfigureContentType();
+ }
+ if (change.Property == ContentTypeProperty ||
+ change.Property == GapProperty)
+ {
+ ConfigureTextRenderTransform();
+ }
+ else if (change.Property == ShapeProperty)
+ {
+ ConfigureShape();
+ }
+ }
+
+ protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
+ {
+ base.OnApplyTemplate(e);
+ _textPresenter = e.NameScope.Find(AvatarThemeConstants.TextPresenterPart);
+ if (_textPresenter != null)
+ {
+ _textPresenter.SizeChanged += (sender, args) =>
+ {
+ ConfigureTextRenderTransform();
+ };
+ }
+ ConfigureShape();
+ ConfigureIconSize();
+ ConfigureContentType();
+ }
+
+ private void ConfigureIconSize()
+ {
+ if (SizeType == AvatarSizeType.Custom)
+ {
+ Debug.Assert(!double.IsNaN(Size));
+ // 不影响模板设置
+ SetValue(WidthProperty, Size, BindingPriority.Template);
+ SetValue(HeightProperty, Size, BindingPriority.Template);
+ SetValue(EffectiveIconSizeProperty,
+ (Icon != null || Src != null) ? Size / 2 : 18, BindingPriority.Template);
+ }
+ }
+
+ private void ConfigureContentType()
+ {
+ if (Src != null)
+ {
+ ContentType = AvatarContentType.SvgImage;
+ }
+ else if (BitmapSrc != null)
+ {
+ ContentType = AvatarContentType.BitmapImage;
+ }
+ else if (Text != null)
+ {
+ ContentType = AvatarContentType.Text;
+ }
+ else
+ {
+ ContentType = AvatarContentType.Icon;
+ }
+ }
+
+ private void ConfigureTextRenderTransform()
+ {
+ if (ContentType != AvatarContentType.Text)
+ {
+ TextRenderTransform = null;
+ }
+ else
+ {
+ if (_textPresenter != null && Gap * 2 < Width)
+ {
+ double scale = 1;
+ double offsetX = 0;
+ var textWidth = TextUtils.CalculateTextSize(Text ?? string.Empty, FontSize, FontFamily).Width;
+ if (Gap * 2 < Width)
+ {
+ scale = (Width - Gap * 2) / textWidth;
+ scale = Math.Min(scale, 1.0);
+ offsetX = (scale * (textWidth - Width)) / 2;
+ }
+ var builder = new TransformOperations.Builder(2);
+ builder.AppendScale(scale, scale);
+ if (scale < 1.0)
+ {
+ builder.AppendTranslate(-offsetX, 0);
+ }
+ TextRenderTransform = builder.Build();
+ }
+ else
+ {
+ TextRenderTransform = null;
+ }
+ }
+ }
+
+ private void ConfigureShape()
+ {
+ if (Shape == AvatarShape.Circle)
+ {
+ SetValue(CornerRadiusProperty, new CornerRadius(Width / 2), BindingPriority.Template);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/AtomUI.Controls/Avatar/AvatarGroup.cs b/src/AtomUI.Controls/Avatar/AvatarGroup.cs
new file mode 100644
index 000000000..6a8a5d3d8
--- /dev/null
+++ b/src/AtomUI.Controls/Avatar/AvatarGroup.cs
@@ -0,0 +1,409 @@
+using System.Collections.Specialized;
+using System.Reactive.Disposables;
+using AtomUI.Data;
+using AtomUI.Theme;
+using AtomUI.Theme.Data;
+using AtomUI.Theme.Styling;
+using AtomUI.Theme.Utils;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Data;
+using Avalonia.Input;
+using Avalonia.Layout;
+using Avalonia.Media;
+using Avalonia.Metadata;
+
+namespace AtomUI.Controls;
+
+using ControlList = Avalonia.Controls.Controls;
+
+public class AvatarGroup : Control,
+ IMotionAwareControl,
+ IControlSharedTokenResourcesHost,
+ IResourceBindingManager
+{
+ #region 公共属性定义
+
+ public static readonly StyledProperty FoldInfoAvatarBackgroundProperty =
+ AvaloniaProperty.Register(nameof (FoldInfoAvatarBackground));
+
+ public static readonly StyledProperty FoldInfoAvatarForegroundProperty =
+ AvaloniaProperty.Register(nameof (FoldInfoAvatarForeground));
+
+ public static readonly StyledProperty FoldAvatarFlyoutTriggerTypeProperty =
+ AvaloniaProperty.Register(nameof(FoldAvatarFlyoutTriggerType), FlyoutTriggerType.Hover);
+
+ public static readonly StyledProperty BorderThicknessProperty =
+ Border.BorderThicknessProperty.AddOwner();
+
+ public static readonly StyledProperty MaxDisplayCountProperty =
+ AvaloniaProperty.Register(nameof(MaxDisplayCount));
+
+ public static readonly StyledProperty SizeTypeProperty =
+ Avatar.SizeTypeProperty.AddOwner();
+
+ public static readonly StyledProperty SizeProperty =
+ Avatar.SizeProperty.AddOwner();
+
+ public static readonly StyledProperty ShapeProperty =
+ Avatar.ShapeProperty.AddOwner();
+
+ public static readonly StyledProperty IsMotionEnabledProperty =
+ MotionAwareControlProperty.IsMotionEnabledProperty.AddOwner();
+
+ public IBrush? FoldInfoAvatarBackground
+ {
+ get => GetValue(FoldInfoAvatarBackgroundProperty);
+ set => SetValue(FoldInfoAvatarBackgroundProperty, value);
+ }
+
+ public IBrush? FoldInfoAvatarForeground
+ {
+ get => GetValue(FoldInfoAvatarForegroundProperty);
+ set => SetValue(FoldInfoAvatarForegroundProperty, value);
+ }
+
+ public FlyoutTriggerType FoldAvatarFlyoutTriggerType
+ {
+ get => GetValue(FoldAvatarFlyoutTriggerTypeProperty);
+ set => SetValue(FoldAvatarFlyoutTriggerTypeProperty, value);
+ }
+
+ public Thickness BorderThickness
+ {
+ get => GetValue(BorderThicknessProperty);
+ set => SetValue(BorderThicknessProperty, value);
+ }
+
+ public int? MaxDisplayCount
+ {
+ get => GetValue(MaxDisplayCountProperty);
+ set => SetValue(MaxDisplayCountProperty, value);
+ }
+
+ public AvatarSizeType SizeType
+ {
+ get => GetValue(SizeTypeProperty);
+ set => SetValue(SizeTypeProperty, value);
+ }
+
+ public double Size
+ {
+ get => GetValue(SizeProperty);
+ set => SetValue(SizeProperty, value);
+ }
+
+ public AvatarShape Shape
+ {
+ get => GetValue(ShapeProperty);
+ set => SetValue(ShapeProperty, value);
+ }
+
+ public bool IsMotionEnabled
+ {
+ get => GetValue(IsMotionEnabledProperty);
+ set => SetValue(IsMotionEnabledProperty, value);
+ }
+
+ [Content] public ControlList Children { get; } = new();
+
+ #endregion
+
+ #region 内部属性定义
+
+ internal static readonly DirectProperty GroupSpaceProperty =
+ AvaloniaProperty.RegisterDirect(
+ nameof(GroupSpace),
+ o => o.GroupSpace,
+ (o, v) => o.GroupSpace = v);
+
+ internal static readonly DirectProperty GroupOverlappingProperty =
+ AvaloniaProperty.RegisterDirect(
+ nameof(GroupOverlapping),
+ o => o.GroupOverlapping,
+ (o, v) => o.GroupOverlapping = v);
+
+ private double _groupSpace;
+
+ internal double GroupSpace
+ {
+ get => _groupSpace;
+ set => SetAndRaise(GroupSpaceProperty, ref _groupSpace, value);
+ }
+
+ private double _groupOverlapping;
+
+ internal double GroupOverlapping
+ {
+ get => _groupOverlapping;
+ set => SetAndRaise(GroupOverlappingProperty, ref _groupOverlapping, value);
+ }
+
+ Control IMotionAwareControl.PropertyBindTarget => this;
+ Control IControlSharedTokenResourcesHost.HostControl => this;
+ string IControlSharedTokenResourcesHost.TokenId => ButtonToken.ID;
+
+ CompositeDisposable? IResourceBindingManager.ResourceBindingsDisposable
+ {
+ get => _resourceBindingsDisposable;
+ set => _resourceBindingsDisposable = value;
+ }
+ #endregion
+
+ private CompositeDisposable? _resourceBindingsDisposable;
+ private Avatar? _foldCountAvatar;
+ private FlyoutHost? _foldCountFlyout;
+ private StackPanel? _foldCountStackPanel;
+ private Dictionary _disposables = new();
+
+ static AvatarGroup()
+ {
+ AffectsMeasure(GroupOverlappingProperty);
+ }
+
+ public AvatarGroup()
+ {
+ Children.CollectionChanged += ChildrenChanged;
+ this.RegisterResources();
+ this.BindMotionProperties();
+ }
+
+ protected virtual void ChildrenChanged(object? sender, NotifyCollectionChangedEventArgs e)
+ {
+ switch (e.Action)
+ {
+ case NotifyCollectionChangedAction.Add:
+ var tobeAdded = e.NewItems!.OfType().ToList();
+ foreach (var item in tobeAdded)
+ {
+ if (item is Avatar avatar)
+ {
+ ConfigureAvatar(avatar);
+ }
+ }
+ if (!MaxDisplayCount.HasValue || LogicalChildren.Count < MaxDisplayCount.Value)
+ {
+ var startingIndex = e.NewStartingIndex;
+ foreach (var item in tobeAdded)
+ {
+ LogicalChildren.Insert(startingIndex, item);
+ VisualChildren.Insert(startingIndex, item);
+ startingIndex++;
+ }
+ }
+ break;
+
+ case NotifyCollectionChangedAction.Move:
+ if (!MaxDisplayCount.HasValue)
+ {
+ LogicalChildren.MoveRange(e.OldStartingIndex, e.OldItems!.Count, e.NewStartingIndex);
+ VisualChildren.MoveRange(e.OldStartingIndex, e.OldItems!.Count, e.NewStartingIndex);
+ }
+ else
+ {
+ if (e.OldStartingIndex < MaxDisplayCount.Value &&
+ e.OldStartingIndex + e.OldItems!.Count <= MaxDisplayCount.Value &&
+ e.NewStartingIndex <= MaxDisplayCount.Value)
+ {
+ LogicalChildren.MoveRange(e.OldStartingIndex, e.OldItems!.Count, e.NewStartingIndex);
+ VisualChildren.MoveRange(e.OldStartingIndex, e.OldItems!.Count, e.NewStartingIndex);
+ }
+ }
+ break;
+
+ case NotifyCollectionChangedAction.Remove:
+ var items = e.OldItems!.OfType().ToList();
+ LogicalChildren.RemoveAll(items);
+ VisualChildren.RemoveAll(items);
+ foreach (var child in items)
+ {
+ if (_disposables.TryGetValue(child, out var disposable))
+ {
+ disposable.Dispose();
+ }
+ _disposables.Remove(child);
+ }
+ break;
+
+ case NotifyCollectionChangedAction.Replace:
+ if (!MaxDisplayCount.HasValue)
+ {
+ for (var i = 0; i < e.OldItems!.Count; ++i)
+ {
+ var index = i + e.OldStartingIndex;
+ var child = (Control)e.NewItems![i]!;
+ LogicalChildren[index] = child;
+ VisualChildren[index] = child;
+ }
+ }
+ else
+ {
+ for (var i = 0; i < e.OldItems!.Count; ++i)
+ {
+ var index = i + e.OldStartingIndex;
+ if (index < MaxDisplayCount.Value)
+ {
+ var child = (Control)e.NewItems![i]!;
+ LogicalChildren[index] = child;
+ VisualChildren[index] = child;
+ }
+ }
+ }
+
+ break;
+
+ case NotifyCollectionChangedAction.Reset:
+ throw new NotSupportedException();
+ }
+
+ var zindex = 1;
+ foreach (var child in Children)
+ {
+ child.ZIndex = zindex++;
+ }
+
+ ConfigureFoldInfo();
+ InvalidateMeasureOnChildrenChanged();
+ }
+
+ private void ConfigureAvatar(Avatar avatar)
+ {
+ var disposable = new CompositeDisposable();
+ disposable.Add(BindUtils.RelayBind(this, BorderThicknessProperty, avatar, BorderThicknessProperty));
+ disposable.Add(BindUtils.RelayBind(this, IsMotionEnabledProperty, avatar, IsMotionEnabledProperty));
+ disposable.Add(BindUtils.RelayBind(this, ShapeProperty, avatar, ShapeProperty));
+ disposable.Add(BindUtils.RelayBind(this, SizeProperty, avatar, SizeProperty));
+ disposable.Add(BindUtils.RelayBind(this, SizeTypeProperty, avatar, SizeTypeProperty));
+ _disposables.Add(avatar, disposable);
+ }
+
+ private Avatar GetFoldCountAvatar()
+ {
+ _foldCountAvatar ??= new Avatar();
+ BindUtils.RelayBind(this, BorderThicknessProperty, _foldCountAvatar, BorderThicknessProperty);
+ BindUtils.RelayBind(this, ShapeProperty, _foldCountAvatar, ShapeProperty);
+ BindUtils.RelayBind(this, SizeProperty, _foldCountAvatar, SizeProperty);
+ BindUtils.RelayBind(this, SizeTypeProperty, _foldCountAvatar, SizeTypeProperty);
+ BindUtils.RelayBind(this, FoldInfoAvatarForegroundProperty, _foldCountAvatar, Avatar.ForegroundProperty);
+ BindUtils.RelayBind(this, FoldInfoAvatarBackgroundProperty, _foldCountAvatar, Avatar.BackgroundProperty);
+ return _foldCountAvatar;
+ }
+
+ protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
+ {
+ base.OnAttachedToVisualTree(e);
+ _resourceBindingsDisposable = new CompositeDisposable();
+ this.AddResourceBindingDisposable(TokenResourceBinder.CreateTokenBinding(this, BorderThicknessProperty,
+ SharedTokenKey.BorderThickness,
+ BindingPriority.Template,
+ new RenderScaleAwareThicknessConfigure(this)));
+ this.AddResourceBindingDisposable(TokenResourceBinder.CreateTokenBinding(this, GroupSpaceProperty,
+ AvatarTokenKey.GroupSpace));
+ this.AddResourceBindingDisposable(TokenResourceBinder.CreateTokenBinding(this, GroupOverlappingProperty,
+ AvatarTokenKey.GroupOverlapping));
+ this.AddResourceBindingDisposable(TokenResourceBinder.CreateTokenBinding(this, FoldInfoAvatarForegroundProperty,
+ AvatarTokenKey.AvatarColor));
+ this.AddResourceBindingDisposable(TokenResourceBinder.CreateTokenBinding(this, FoldInfoAvatarBackgroundProperty,
+ AvatarTokenKey.AvatarBg));
+ var foldCountAvatar = GetFoldCountAvatar();
+ if (_foldCountFlyout == null)
+ {
+ _foldCountFlyout = new FlyoutHost();
+ _foldCountFlyout.ZIndex = Int32.MaxValue;
+ _foldCountFlyout.AnchorTarget = foldCountAvatar;
+ _foldCountStackPanel = new StackPanel();
+ _foldCountStackPanel.Orientation = Orientation.Horizontal;
+ BindUtils.RelayBind(this, GroupSpaceProperty, _foldCountStackPanel, StackPanel.SpacingProperty);
+ BindUtils.RelayBind(this, FoldAvatarFlyoutTriggerTypeProperty, _foldCountFlyout, FlyoutHost.TriggerProperty);
+ _foldCountFlyout.Flyout = new Flyout()
+ {
+ Content = _foldCountStackPanel
+ };
+ }
+
+ ConfigureFoldInfo();
+ }
+
+ private void ConfigureFoldInfo()
+ {
+ var foldCountAvatar = GetFoldCountAvatar();
+ if (_foldCountFlyout != null)
+ {
+ if (MaxDisplayCount.HasValue && Children.Count > MaxDisplayCount.Value)
+ {
+ foldCountAvatar.Text = $"+{Children.Count - MaxDisplayCount.Value}";
+ LogicalChildren.Add(_foldCountFlyout);
+ VisualChildren.Add(_foldCountFlyout);
+ for (var i = MaxDisplayCount.Value; i < Children.Count; ++i)
+ {
+ _foldCountStackPanel?.Children.Add(Children[i]);
+ }
+ }
+ else
+ {
+ LogicalChildren.Remove(_foldCountFlyout);
+ VisualChildren.Remove(_foldCountFlyout);
+ _foldCountStackPanel?.Children.Clear();
+ }
+ }
+ }
+
+ protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
+ {
+ base.OnDetachedFromVisualTree(e);
+ this.DisposeTokenBindings();
+ }
+
+ private protected virtual void InvalidateMeasureOnChildrenChanged()
+ {
+ InvalidateMeasure();
+ }
+
+ protected override Size MeasureOverride(Size availableSize)
+ {
+ var size = base.MeasureOverride(availableSize);
+ // 理论上是统一的,我们的孩子都一样大
+ var count = LogicalChildren.Count;
+ var totalWidth = count * size.Width - (count - 1) * GroupOverlapping;
+ return size.WithWidth(totalWidth);
+ }
+
+ protected override Size ArrangeOverride(Size finalSize)
+ {
+ var offetX = 0.0;
+ for (var i = 0; i < LogicalChildren.Count; i++)
+ {
+ var child = LogicalChildren[i];
+ if (child is Control avatar)
+ {
+ var childSize = avatar.DesiredSize;
+ avatar.Arrange(new Rect(offetX, 0, childSize.Width, childSize.Height));
+ offetX += childSize.Width - GroupOverlapping;
+ }
+ }
+ return finalSize;
+ }
+
+ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
+ {
+ base.OnPropertyChanged(change);
+ if (change.Property == FoldAvatarFlyoutTriggerTypeProperty)
+ {
+ ConfigureFoldAvatarCursor();
+ }
+ }
+
+ private void ConfigureFoldAvatarCursor()
+ {
+ var foldCountAvatar = GetFoldCountAvatar();
+ if (FoldAvatarFlyoutTriggerType == FlyoutTriggerType.Click)
+ {
+
+ foldCountAvatar.Cursor = new Cursor(StandardCursorType.Hand);
+ }
+ else
+ {
+ foldCountAvatar.Cursor = new Cursor(StandardCursorType.Arrow);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/AtomUI.Controls/Avatar/AvatarPseudoClass.cs b/src/AtomUI.Controls/Avatar/AvatarPseudoClass.cs
new file mode 100644
index 000000000..b875d2e80
--- /dev/null
+++ b/src/AtomUI.Controls/Avatar/AvatarPseudoClass.cs
@@ -0,0 +1,9 @@
+namespace AtomUI.Controls;
+
+public static class AvatarPseudoClass
+{
+ public const string Large = ":large";
+ public const string Middle = ":middle";
+ public const string Small = ":small";
+ public const string Custom = ":custom-size";
+}
\ No newline at end of file
diff --git a/src/AtomUI.Controls/Avatar/AvatarToken.cs b/src/AtomUI.Controls/Avatar/AvatarToken.cs
new file mode 100644
index 000000000..8487b18a0
--- /dev/null
+++ b/src/AtomUI.Controls/Avatar/AvatarToken.cs
@@ -0,0 +1,81 @@
+using AtomUI.Theme.TokenSystem;
+using Avalonia.Media;
+
+namespace AtomUI.Controls;
+
+[ControlDesignToken]
+internal class AvatarToken : AbstractControlDesignToken
+{
+ public const string ID = "Avatar";
+
+ ///
+ /// 头像尺寸
+ ///
+ public double ContainerSize { get; set; }
+
+ ///
+ /// 大号头像尺寸
+ ///
+ public double ContainerSizeLG { get; set; }
+
+ ///
+ /// 小号头像尺寸
+ ///
+ public double ContainerSizeSM { get; set; }
+
+ ///
+ /// 头像文字大小
+ ///
+ public double TextFontSize { get; set; }
+
+ ///
+ /// 大号头像文字大小
+ ///
+ public double TextFontSizeLG { get; set; }
+
+ ///
+ /// 小号头像文字大小
+ ///
+ public double TextFontSizeSM { get; set; }
+
+ ///
+ /// 头像组间距
+ ///
+ public double GroupSpace { get; set; }
+
+ ///
+ /// 头像组重叠宽度
+ ///
+ public double GroupOverlapping { get; set; }
+
+ ///
+ /// 头像组边框颜色
+ ///
+ public Color GroupBorderColor { get; set; }
+
+ // 内部使用别名
+ public Color AvatarBg { get; set; }
+ public Color AvatarColor { get; set; }
+
+ public AvatarToken()
+ : base(ID)
+ {
+ }
+
+ protected internal override void CalculateFromAlias()
+ {
+ base.CalculateFromAlias();
+ ContainerSize = SharedToken.ControlHeight;
+ ContainerSizeLG = SharedToken.ControlHeightLG;
+ ContainerSizeSM = SharedToken.ControlHeightSM;
+ TextFontSize = Math.Round((SharedToken.FontSizeLG + SharedToken.FontSizeXL) / 2);
+ TextFontSizeLG = SharedToken.FontSizeHeading3;
+ TextFontSizeSM = SharedToken.FontSize;
+ GroupSpace = SharedToken.UniformlyMarginXXS;
+ GroupOverlapping = SharedToken.UniformlyMarginXS;
+ GroupBorderColor = SharedToken.ColorBorderBg;
+
+ AvatarBg = SharedToken.ColorTextPlaceholder;
+ AvatarColor = SharedToken.ColorTextLightSolid;
+ }
+}
\ No newline at end of file
diff --git a/src/AtomUI.Controls/Avatar/Themes/AvatarTheme.axaml b/src/AtomUI.Controls/Avatar/Themes/AvatarTheme.axaml
new file mode 100644
index 000000000..675287a2c
--- /dev/null
+++ b/src/AtomUI.Controls/Avatar/Themes/AvatarTheme.axaml
@@ -0,0 +1,118 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/AtomUI.Controls/Avatar/Themes/AvatarThemeConstants.cs b/src/AtomUI.Controls/Avatar/Themes/AvatarThemeConstants.cs
new file mode 100644
index 000000000..0ba19f09e
--- /dev/null
+++ b/src/AtomUI.Controls/Avatar/Themes/AvatarThemeConstants.cs
@@ -0,0 +1,6 @@
+namespace AtomUI.Controls.Themes;
+
+internal static class AvatarThemeConstants
+{
+ public const string TextPresenterPart = "PART_TextPresenter";
+}
\ No newline at end of file
diff --git a/src/AtomUI.Controls/Avatar/Themes/AvatarThemes.axaml b/src/AtomUI.Controls/Avatar/Themes/AvatarThemes.axaml
new file mode 100644
index 000000000..beb68fe75
--- /dev/null
+++ b/src/AtomUI.Controls/Avatar/Themes/AvatarThemes.axaml
@@ -0,0 +1,6 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/src/AtomUI.Controls/GeneratedFiles/AtomUI.Generator/AtomUI.Generator.TokenResourceKeyGenerator/ControlTokenTypePool.g.cs b/src/AtomUI.Controls/GeneratedFiles/AtomUI.Generator/AtomUI.Generator.TokenResourceKeyGenerator/ControlTokenTypePool.g.cs
index bf0b4fe3e..417b13fe7 100644
--- a/src/AtomUI.Controls/GeneratedFiles/AtomUI.Generator/AtomUI.Generator.TokenResourceKeyGenerator/ControlTokenTypePool.g.cs
+++ b/src/AtomUI.Controls/GeneratedFiles/AtomUI.Generator/AtomUI.Generator.TokenResourceKeyGenerator/ControlTokenTypePool.g.cs
@@ -12,6 +12,7 @@ namespace AtomUI.Theme
tokenTypes.Add(typeof(AtomUI.Controls.AdornerLayerToken));
tokenTypes.Add(typeof(AtomUI.Controls.AlertToken));
tokenTypes.Add(typeof(AtomUI.Controls.ArrowDecoratedBoxToken));
+ tokenTypes.Add(typeof(AtomUI.Controls.AvatarToken));
tokenTypes.Add(typeof(AtomUI.Controls.BadgeToken));
tokenTypes.Add(typeof(AtomUI.Controls.ButtonSpinnerToken));
tokenTypes.Add(typeof(AtomUI.Controls.ButtonToken));
diff --git a/src/AtomUI.Controls/GeneratedFiles/AtomUI.Generator/AtomUI.Generator.TokenResourceKeyGenerator/TokenResourceConst.g.cs b/src/AtomUI.Controls/GeneratedFiles/AtomUI.Generator/AtomUI.Generator.TokenResourceKeyGenerator/TokenResourceConst.g.cs
index ced059d4d..4269f171f 100644
--- a/src/AtomUI.Controls/GeneratedFiles/AtomUI.Generator/AtomUI.Generator.TokenResourceKeyGenerator/TokenResourceConst.g.cs
+++ b/src/AtomUI.Controls/GeneratedFiles/AtomUI.Generator/AtomUI.Generator.TokenResourceKeyGenerator/TokenResourceConst.g.cs
@@ -47,6 +47,21 @@ namespace AtomUI.Theme.Styling
public static readonly TokenResourceKey Padding = new TokenResourceKey("ArrowDecoratedBox.Padding");
}
+ public static class AvatarTokenKey
+ {
+ public static readonly TokenResourceKey AvatarBg = new TokenResourceKey("Avatar.AvatarBg");
+ public static readonly TokenResourceKey AvatarColor = new TokenResourceKey("Avatar.AvatarColor");
+ public static readonly TokenResourceKey ContainerSize = new TokenResourceKey("Avatar.ContainerSize");
+ public static readonly TokenResourceKey ContainerSizeLG = new TokenResourceKey("Avatar.ContainerSizeLG");
+ public static readonly TokenResourceKey ContainerSizeSM = new TokenResourceKey("Avatar.ContainerSizeSM");
+ public static readonly TokenResourceKey GroupBorderColor = new TokenResourceKey("Avatar.GroupBorderColor");
+ public static readonly TokenResourceKey GroupOverlapping = new TokenResourceKey("Avatar.GroupOverlapping");
+ public static readonly TokenResourceKey GroupSpace = new TokenResourceKey("Avatar.GroupSpace");
+ public static readonly TokenResourceKey TextFontSize = new TokenResourceKey("Avatar.TextFontSize");
+ public static readonly TokenResourceKey TextFontSizeLG = new TokenResourceKey("Avatar.TextFontSizeLG");
+ public static readonly TokenResourceKey TextFontSizeSM = new TokenResourceKey("Avatar.TextFontSizeSM");
+ }
+
public static class BadgeTokenKey
{
public static readonly TokenResourceKey BadgeColor = new TokenResourceKey("Badge.BadgeColor");