diff --git a/samples/AtomUI.Demo.Desktop/ShowCase/TabControlShowCase.axaml b/samples/AtomUI.Demo.Desktop/ShowCase/TabControlShowCase.axaml index 2134d7294..32ddbdd9d 100644 --- a/samples/AtomUI.Demo.Desktop/ShowCase/TabControlShowCase.axaml +++ b/samples/AtomUI.Demo.Desktop/ShowCase/TabControlShowCase.axaml @@ -31,5 +31,29 @@ + + + + + Tab 1 + Tab 2 + Tab 3 + + + + + + + + Tab 1 + Tab 2 + Tab 3 + + + \ No newline at end of file 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 12686a5ee..5a370f47d 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 @@ -314,6 +314,7 @@ namespace AtomUI.Theme.Styling public static readonly TokenResourceKey ItemHoverColor = new TokenResourceKey("TabControl.ItemHoverColor"); public static readonly TokenResourceKey ItemSelectedColor = new TokenResourceKey("TabControl.ItemSelectedColor"); public static readonly TokenResourceKey CardGutter = new TokenResourceKey("TabControl.CardGutter"); + public static readonly TokenResourceKey ItemIconMargin = new TokenResourceKey("TabControl.ItemIconMargin"); } public static class TagResourceKey diff --git a/src/AtomUI.Controls/PathIcon/PathIcon.cs b/src/AtomUI.Controls/PathIcon/PathIcon.cs index d1af9821a..2222ff0b4 100644 --- a/src/AtomUI.Controls/PathIcon/PathIcon.cs +++ b/src/AtomUI.Controls/PathIcon/PathIcon.cs @@ -2,6 +2,7 @@ using Avalonia; using Avalonia.Animation; using Avalonia.Controls; +using Avalonia.Layout; using Avalonia.Media; using Avalonia.Reactive; using Avalonia.Rendering; @@ -145,6 +146,8 @@ public sealed class PathIcon : Control, ICustomHitTest FilledBrushProperty, PrimaryFilledBrushProperty, SecondaryFilledBrushProperty); + HorizontalAlignmentProperty.OverrideDefaultValue(HorizontalAlignment.Left); + VerticalAlignmentProperty.OverrideDefaultValue(VerticalAlignment.Top); } public PathIcon() diff --git a/src/AtomUI.Controls/TabControl/BaseTabStrip.cs b/src/AtomUI.Controls/TabControl/BaseTabStrip.cs index 6e9af577d..9f9fe88d3 100644 --- a/src/AtomUI.Controls/TabControl/BaseTabStrip.cs +++ b/src/AtomUI.Controls/TabControl/BaseTabStrip.cs @@ -1,5 +1,4 @@ -using AtomUI.Theme.Data; -using AtomUI.Theme.Styling; +using AtomUI.Theme.Styling; using AtomUI.Utils; using Avalonia; using Avalonia.Controls; @@ -29,6 +28,9 @@ public abstract class BaseTabStrip : AvaloniaTabStrip, ISizeTypeAware public static readonly StyledProperty TabStripPlacementProperty = AvaloniaProperty.Register(nameof(TabStripPlacement), defaultValue: Dock.Top); + + public static readonly StyledProperty TabAlignmentCenterProperty = + AvaloniaProperty.Register(nameof(TabAlignmentCenter), false); public SizeType SizeType { @@ -42,6 +44,12 @@ public abstract class BaseTabStrip : AvaloniaTabStrip, ISizeTypeAware set => SetValue(TabStripPlacementProperty, value); } + public bool TabAlignmentCenter + { + get => GetValue(TabAlignmentCenterProperty); + set => SetValue(TabAlignmentCenterProperty, value); + } + #endregion private Border? _mainContainer; diff --git a/src/AtomUI.Controls/TabControl/BaseTabStripItemTheme.cs b/src/AtomUI.Controls/TabControl/BaseTabStripItemTheme.cs index 38ebc2b5a..a9a11205e 100644 --- a/src/AtomUI.Controls/TabControl/BaseTabStripItemTheme.cs +++ b/src/AtomUI.Controls/TabControl/BaseTabStripItemTheme.cs @@ -1,4 +1,5 @@ -using AtomUI.Theme; +using AtomUI.Icon; +using AtomUI.Theme; using AtomUI.Theme.Styling; using Avalonia.Controls; using Avalonia.Controls.Presenters; @@ -14,6 +15,7 @@ internal class BaseTabStripItemTheme : BaseControlTheme public const string DecoratorPart = "Part_Decorator"; public const string ContentLayoutPart = "Part_ContentLayout"; public const string ContentPresenterPart = "PART_ContentPresenter"; + public const string ItemIconPart = "PART_ItemIcon"; public BaseTabStripItemTheme(Type targetType) : base(targetType) { } @@ -62,16 +64,26 @@ internal class BaseTabStripItemTheme : BaseControlTheme commonStyle.Add(TabStripItem.CursorProperty, new Cursor(StandardCursorType.Hand)); commonStyle.Add(TabStripItem.ForegroundProperty, TabControlResourceKey.ItemColor); - // 增加文字 hover 和 选中的效果 + // hover var hoverStyle = new Style(selector => selector.Nesting().Class(StdPseudoClass.PointerOver)); hoverStyle.Add(TabStripItem.ForegroundProperty, TabControlResourceKey.ItemHoverColor); + { + var iconStyle = new Style(selector => selector.Nesting().Template().Name(ItemIconPart)); + iconStyle.Add(PathIcon.IconModeProperty, IconMode.Active); + hoverStyle.Add(iconStyle); + } + commonStyle.Add(hoverStyle); // 选中 var selectedStyle = new Style(selector => selector.Nesting().Class(StdPseudoClass.Selected)); selectedStyle.Add(TabStripItem.ForegroundProperty, TabControlResourceKey.ItemSelectedColor); + { + var iconStyle = new Style(selector => selector.Nesting().Template().Name(ItemIconPart)); + iconStyle.Add(PathIcon.IconModeProperty, IconMode.Selected); + selectedStyle.Add(iconStyle); + } commonStyle.Add(selectedStyle); - Add(commonStyle); BuildSizeTypeStyle(); BuildDisabledStyle(); diff --git a/src/AtomUI.Controls/TabControl/TabControlToken.cs b/src/AtomUI.Controls/TabControl/TabControlToken.cs index d040482a6..f0db77d18 100644 --- a/src/AtomUI.Controls/TabControl/TabControlToken.cs +++ b/src/AtomUI.Controls/TabControl/TabControlToken.cs @@ -116,6 +116,11 @@ internal class TabControlToken : AbstractControlDesignToken /// 卡片标签间距 /// public double CardGutter { get; set; } + + /// + /// 标签内容 icon 的外边距 + /// + public Thickness ItemIconMargin { get; set; } internal override void CalculateFromAlias() { @@ -152,5 +157,6 @@ internal class TabControlToken : AbstractControlDesignToken ItemHoverColor = colorToken.ColorPrimaryToken.ColorPrimaryHover; CardGutter = _globalToken.MarginXXS / 2; + ItemIconMargin = new Thickness(0, 0, _globalToken.MarginSM, 0); } } \ No newline at end of file diff --git a/src/AtomUI.Controls/TabControl/TabStrip.cs b/src/AtomUI.Controls/TabControl/TabStrip.cs index 517267222..77f30bd6f 100644 --- a/src/AtomUI.Controls/TabControl/TabStrip.cs +++ b/src/AtomUI.Controls/TabControl/TabStrip.cs @@ -4,6 +4,7 @@ using Avalonia; using Avalonia.Animation; using Avalonia.Animation.Easings; using Avalonia.Controls; +using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Media.Transformation; @@ -12,10 +13,12 @@ namespace AtomUI.Controls; public class TabStrip : BaseTabStrip { private Border? _selectedIndicator; + private ItemsPresenter? _itemsPresenter; public TabStrip() { SelectionChanged += HandleSelectionChanged; + LayoutUpdated += HandleLayoutUpdated; } private void HandleSelectionChanged(object? sender, SelectionChangedEventArgs args) @@ -25,15 +28,29 @@ public class TabStrip : BaseTabStrip } } + private void HandleLayoutUpdated(object? sender, EventArgs args) + { + if (_selectedIndicator is not null) { + if (_selectedIndicator.Transitions is null) { + var transitions = new Transitions(); + transitions.Add(AnimationUtils.CreateTransition(Border.RenderTransformProperty, + GlobalResourceKey.MotionDurationSlow, new ExponentialEaseOut())); + _selectedIndicator.Transitions = transitions; + // 只需要执行一次 + LayoutUpdated -= HandleLayoutUpdated; + } + } + } + private void SetupSelectedIndicator() { if (_selectedIndicator is not null && SelectedItem is TabStripItem tabStripItem) { var selectedBounds = tabStripItem.Bounds; var builder = new TransformOperations.Builder(1); - + var offset = _itemsPresenter?.Bounds.Position ?? default; if (TabStripPlacement == Dock.Top) { _selectedIndicator.Width = tabStripItem.DesiredSize.Width; - builder.AppendTranslate(selectedBounds.Left, 0); + builder.AppendTranslate(offset.X + selectedBounds.Left, 0); } else if (TabStripPlacement == Dock.Right) { _selectedIndicator.Height = tabStripItem.DesiredSize.Height; } else if (TabStripPlacement == Dock.Bottom) { @@ -45,9 +62,9 @@ public class TabStrip : BaseTabStrip } } - protected override Size MeasureOverride(Size availableSize) + protected override Size ArrangeOverride(Size finalSize) { - var size = base.MeasureOverride(availableSize); + var size = base.ArrangeOverride(finalSize); if (SelectedItem is TabStripItem) { SetupSelectedIndicator(); } @@ -56,7 +73,7 @@ public class TabStrip : BaseTabStrip protected override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey) { - var tabStripItem = new TabStripItem() + var tabStripItem = new TabStripItem { Shape = TabSharp.Line }; @@ -67,13 +84,6 @@ public class TabStrip : BaseTabStrip { base.OnApplyTemplate(e); _selectedIndicator = e.NameScope.Find(TabStripTheme.SelectedItemIndicatorPart); - if (_selectedIndicator is not null) { - if (_selectedIndicator.Transitions is null) { - var transitions = new Transitions(); - transitions.Add(AnimationUtils.CreateTransition(Border.RenderTransformProperty, - GlobalResourceKey.MotionDurationSlow, new ExponentialEaseOut())); - _selectedIndicator.Transitions = transitions; - } - } + _itemsPresenter = e.NameScope.Find(TabStripTheme.ItemsPresenterPart); } } \ No newline at end of file diff --git a/src/AtomUI.Controls/TabControl/TabStripItem.cs b/src/AtomUI.Controls/TabControl/TabStripItem.cs index 11ecdb0a5..0e2109e07 100644 --- a/src/AtomUI.Controls/TabControl/TabStripItem.cs +++ b/src/AtomUI.Controls/TabControl/TabStripItem.cs @@ -1,4 +1,5 @@ using AtomUI.Controls.Utils; +using AtomUI.Icon; using AtomUI.Media; using AtomUI.Theme.Styling; using AtomUI.Theme.Utils; @@ -8,6 +9,7 @@ using Avalonia.Animation; using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.LogicalTree; +using Avalonia.Rendering; namespace AtomUI.Controls; @@ -19,7 +21,7 @@ public enum TabSharp Card } -public class TabStripItem : AvaloniaTabStripItem, IControlCustomStyle +public class TabStripItem : AvaloniaTabStripItem, IControlCustomStyle, ICustomHitTest { #region 公共属性定义 public static readonly StyledProperty SizeTypeProperty = @@ -92,6 +94,16 @@ public class TabStripItem : AvaloniaTabStripItem, IControlCustomStyle { if (Icon is not null) { UIStructureUtils.SetTemplateParent(Icon, this); + Icon.Name = TabStripItemTheme.ItemIconPart; + if (Icon.ThemeType != IconThemeType.TwoTone) { + TokenResourceBinder.CreateTokenBinding(Icon, PathIcon.NormalFilledBrushProperty, TabControlResourceKey.ItemColor); + TokenResourceBinder.CreateTokenBinding(Icon, PathIcon.ActiveFilledBrushProperty, TabControlResourceKey.ItemHoverColor); + TokenResourceBinder.CreateTokenBinding(Icon, PathIcon.SelectedFilledBrushProperty, TabControlResourceKey.ItemSelectedColor); + TokenResourceBinder.CreateTokenBinding(Icon, PathIcon.DisabledFilledBrushProperty, GlobalResourceKey.ColorTextDisabled); + } + if (_contentLayout is not null) { + _contentLayout.Children.Insert(0, Icon); + } } } @@ -124,9 +136,8 @@ public class TabStripItem : AvaloniaTabStripItem, IControlCustomStyle if (oldIcon != null) { UIStructureUtils.SetTemplateParent(oldIcon, null); } - if (Icon is not null) { - UIStructureUtils.SetTemplateParent(Icon, this); - } + + SetupItemIcon(); } } } @@ -140,4 +151,9 @@ public class TabStripItem : AvaloniaTabStripItem, IControlCustomStyle TokenResourceBinder.CreateTokenBinding(this, ThemeProperty, CardTabStripItemTheme.ID); } } + + public bool HitTest(Point point) + { + return true; + } } \ No newline at end of file diff --git a/src/AtomUI.Controls/TabControl/TabStripItemTheme.cs b/src/AtomUI.Controls/TabControl/TabStripItemTheme.cs index 4ceea5893..89e207f8d 100644 --- a/src/AtomUI.Controls/TabControl/TabStripItemTheme.cs +++ b/src/AtomUI.Controls/TabControl/TabStripItemTheme.cs @@ -1,5 +1,6 @@ using AtomUI.Theme.Styling; using Avalonia.Controls; +using Avalonia.Layout; using Avalonia.Styling; namespace AtomUI.Controls; @@ -19,10 +20,19 @@ internal class TabStripItemTheme : BaseTabStripItemTheme protected override void BuildStyles() { base.BuildStyles(); + + // Icon 一些通用属性 + { + var iconStyle = new Style(selector => selector.Nesting().Template().OfType()); + iconStyle.Add(PathIcon.MarginProperty, TabControlResourceKey.ItemIconMargin); + Add(iconStyle); + } + var decoratorStyle = new Style(selector => selector.Nesting().Template().OfType().Name(DecoratorPart)); decoratorStyle.Add(Border.MarginProperty, TabControlResourceKey.HorizontalItemMargin); Add(decoratorStyle); BuildSizeTypeStyle(); + BuildPlacementStyle(); } protected void BuildSizeTypeStyle() @@ -33,6 +43,13 @@ internal class TabStripItemTheme : BaseTabStripItemTheme decoratorStyle.Add(Border.PaddingProperty, TabControlResourceKey.HorizontalItemPaddingLG); largeSizeStyle.Add(decoratorStyle); } + { + // Icon + var iconStyle = new Style(selector => selector.Nesting().Template().Name(ItemIconPart)); + iconStyle.Add(PathIcon.WidthProperty, GlobalResourceKey.IconSize); + iconStyle.Add(PathIcon.HeightProperty, GlobalResourceKey.IconSize); + largeSizeStyle.Add(iconStyle); + } Add(largeSizeStyle); var middleSizeStyle = new Style(selector => selector.Nesting().PropertyEquals(TabStripItem.SizeTypeProperty, SizeType.Middle)); @@ -41,6 +58,13 @@ internal class TabStripItemTheme : BaseTabStripItemTheme decoratorStyle.Add(Border.PaddingProperty, TabControlResourceKey.HorizontalItemPadding); middleSizeStyle.Add(decoratorStyle); } + { + // Icon + var iconStyle = new Style(selector => selector.Nesting().Template().Name(ItemIconPart)); + iconStyle.Add(PathIcon.WidthProperty, GlobalResourceKey.IconSize); + iconStyle.Add(PathIcon.HeightProperty, GlobalResourceKey.IconSize); + middleSizeStyle.Add(iconStyle); + } Add(middleSizeStyle); var smallSizeType = new Style(selector => selector.Nesting().PropertyEquals(TabStripItem.SizeTypeProperty, SizeType.Small)); @@ -49,6 +73,53 @@ internal class TabStripItemTheme : BaseTabStripItemTheme decoratorStyle.Add(Border.PaddingProperty, TabControlResourceKey.HorizontalItemPaddingSM); smallSizeType.Add(decoratorStyle); } + { + // Icon + var iconStyle = new Style(selector => selector.Nesting().Template().Name(ItemIconPart)); + iconStyle.Add(PathIcon.WidthProperty, GlobalResourceKey.IconSizeSM); + iconStyle.Add(PathIcon.HeightProperty, GlobalResourceKey.IconSizeSM); + smallSizeType.Add(iconStyle); + } Add(smallSizeType); } + + private void BuildPlacementStyle() + { + // 设置 items presenter 面板样式 + // 分为上、右、下、左 + { + // 上 + var topStyle = new Style(selector => selector.Nesting().PropertyEquals(TabStripItem.TabStripPlacementProperty, Dock.Top)); + var iconStyle = new Style(selector => selector.Nesting().Template().OfType()); + iconStyle.Add(PathIcon.VerticalAlignmentProperty, VerticalAlignment.Center); + topStyle.Add(iconStyle); + Add(topStyle); + } + + { + // 右 + var rightStyle = new Style(selector => selector.Nesting().PropertyEquals(TabStripItem.TabStripPlacementProperty, Dock.Right)); + var iconStyle = new Style(selector => selector.Nesting().Template().OfType()); + iconStyle.Add(PathIcon.HorizontalAlignmentProperty, HorizontalAlignment.Center); + rightStyle.Add(iconStyle); + Add(rightStyle); + } + { + // 下 + var bottomStyle = new Style(selector => selector.Nesting().PropertyEquals(TabStripItem.TabStripPlacementProperty, Dock.Bottom)); + + var iconStyle = new Style(selector => selector.Nesting().Template().OfType()); + iconStyle.Add(PathIcon.VerticalAlignmentProperty, VerticalAlignment.Center); + bottomStyle.Add(iconStyle); + Add(bottomStyle); + } + { + // 左 + var leftStyle = new Style(selector => selector.Nesting().PropertyEquals(TabStripItem.TabStripPlacementProperty, Dock.Left)); + var iconStyle = new Style(selector => selector.Nesting().Template().OfType()); + iconStyle.Add(PathIcon.HorizontalAlignmentProperty, HorizontalAlignment.Center); + leftStyle.Add(iconStyle); + Add(leftStyle); + } + } } \ No newline at end of file diff --git a/src/AtomUI.Controls/TabControl/TabStripTheme.cs b/src/AtomUI.Controls/TabControl/TabStripTheme.cs index 0d141d76d..1d2dea731 100644 --- a/src/AtomUI.Controls/TabControl/TabStripTheme.cs +++ b/src/AtomUI.Controls/TabControl/TabStripTheme.cs @@ -18,7 +18,7 @@ internal class TabStripTheme : BaseTabStripTheme protected override void NotifyBuildControlTemplate(BaseTabStrip baseTabStrip, INameScope scope, Border container) { var layout = new Panel(); - var itemsPresenter = new ItemsPresenter() + var itemsPresenter = new ItemsPresenter { Name = ItemsPresenterPart, }; @@ -55,6 +55,13 @@ internal class TabStripTheme : BaseTabStripTheme indicatorStyle.Add(Border.VerticalAlignmentProperty, VerticalAlignment.Bottom); topStyle.Add(indicatorStyle); + // tabs 是否居中 + var tabAlignCenterStyle = new Style(selector => selector.Nesting().PropertyEquals(TabStrip.TabAlignmentCenterProperty, true)); + var itemsPresenterStyle = new Style(selector => selector.Nesting().Template().Name(ItemsPresenterPart)); + itemsPresenterStyle.Add(ItemsPresenter.HorizontalAlignmentProperty, HorizontalAlignment.Center); + tabAlignCenterStyle.Add(itemsPresenterStyle); + topStyle.Add(tabAlignCenterStyle); + topStyle.Add(itemPresenterPanelStyle); commonStyle.Add(topStyle); } @@ -72,6 +79,13 @@ internal class TabStripTheme : BaseTabStripTheme indicatorStyle.Add(Border.VerticalAlignmentProperty, VerticalAlignment.Top); rightStyle.Add(indicatorStyle); + // tabs 是否居中 + var tabAlignCenterStyle = new Style(selector => selector.Nesting().PropertyEquals(TabStrip.TabAlignmentCenterProperty, true)); + var itemsPresenterStyle = new Style(selector => selector.Nesting().Template().Name(ItemsPresenterPart)); + itemsPresenterStyle.Add(ItemsPresenter.VerticalAlignmentProperty, VerticalAlignment.Center); + tabAlignCenterStyle.Add(itemsPresenterStyle); + rightStyle.Add(tabAlignCenterStyle); + commonStyle.Add(rightStyle); } { @@ -87,6 +101,13 @@ internal class TabStripTheme : BaseTabStripTheme indicatorStyle.Add(Border.VerticalAlignmentProperty, VerticalAlignment.Top); bottomStyle.Add(indicatorStyle); + // tabs 是否居中 + var tabAlignCenterStyle = new Style(selector => selector.Nesting().PropertyEquals(TabStrip.TabAlignmentCenterProperty, true)); + var itemsPresenterStyle = new Style(selector => selector.Nesting().Template().Name(ItemsPresenterPart)); + itemsPresenterStyle.Add(ItemsPresenter.HorizontalAlignmentProperty, HorizontalAlignment.Center); + tabAlignCenterStyle.Add(itemsPresenterStyle); + bottomStyle.Add(tabAlignCenterStyle); + commonStyle.Add(bottomStyle); } { @@ -102,6 +123,13 @@ internal class TabStripTheme : BaseTabStripTheme indicatorStyle.Add(Border.VerticalAlignmentProperty, VerticalAlignment.Top); leftStyle.Add(indicatorStyle); + // tabs 是否居中 + var tabAlignCenterStyle = new Style(selector => selector.Nesting().PropertyEquals(TabStrip.TabAlignmentCenterProperty, true)); + var itemsPresenterStyle = new Style(selector => selector.Nesting().Template().Name(ItemsPresenterPart)); + itemsPresenterStyle.Add(ItemsPresenter.VerticalAlignmentProperty, VerticalAlignment.Center); + tabAlignCenterStyle.Add(itemsPresenterStyle); + leftStyle.Add(tabAlignCenterStyle); + commonStyle.Add(leftStyle); }