diff --git a/samples/AtomUI.Demo.Desktop/ShowCase/MenuShowCase.axaml b/samples/AtomUI.Demo.Desktop/ShowCase/MenuShowCase.axaml index 9242db063..943c4dc33 100644 --- a/samples/AtomUI.Demo.Desktop/ShowCase/MenuShowCase.axaml +++ b/samples/AtomUI.Demo.Desktop/ShowCase/MenuShowCase.axaml @@ -193,7 +193,7 @@ Description="Horizontal top navigation menu."> - + 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 efc17e48f..50dfa8eca 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 @@ -396,8 +396,8 @@ namespace AtomUI.Theme.Styling public static readonly TokenResourceKey HorizontalItemSelectedBg = new TokenResourceKey("NavMenu.HorizontalItemSelectedBg", "AtomUI.Token"); public static readonly TokenResourceKey ActiveBarWidth = new TokenResourceKey("NavMenu.ActiveBarWidth", "AtomUI.Token"); public static readonly TokenResourceKey ActiveBarHeight = new TokenResourceKey("NavMenu.ActiveBarHeight", "AtomUI.Token"); - public static readonly TokenResourceKey ActiveBarBorderWidth = new TokenResourceKey("NavMenu.ActiveBarBorderWidth", "AtomUI.Token"); public static readonly TokenResourceKey ItemMargin = new TokenResourceKey("NavMenu.ItemMargin", "AtomUI.Token"); + public static readonly TokenResourceKey HorizontalItemMargin = new TokenResourceKey("NavMenu.HorizontalItemMargin", "AtomUI.Token"); public static readonly TokenResourceKey ItemPadding = new TokenResourceKey("NavMenu.ItemPadding", "AtomUI.Token"); public static readonly TokenResourceKey HorizontalItemHoverBg = new TokenResourceKey("NavMenu.HorizontalItemHoverBg", "AtomUI.Token"); public static readonly TokenResourceKey HorizontalItemBorderRadius = new TokenResourceKey("NavMenu.HorizontalItemBorderRadius", "AtomUI.Token"); @@ -423,7 +423,6 @@ namespace AtomUI.Theme.Styling public static readonly TokenResourceKey DarkDangerItemHoverColor = new TokenResourceKey("NavMenu.DarkDangerItemHoverColor", "AtomUI.Token"); public static readonly TokenResourceKey DarkDangerItemSelectedColor = new TokenResourceKey("NavMenu.DarkDangerItemSelectedColor", "AtomUI.Token"); public static readonly TokenResourceKey DarkDangerItemActiveBg = new TokenResourceKey("NavMenu.DarkDangerItemActiveBg", "AtomUI.Token"); - public static readonly TokenResourceKey ItemWidth = new TokenResourceKey("NavMenu.ItemWidth", "AtomUI.Token"); public static readonly TokenResourceKey MenuHorizontalHeight = new TokenResourceKey("NavMenu.MenuHorizontalHeight", "AtomUI.Token"); public static readonly TokenResourceKey MenuArrowSize = new TokenResourceKey("NavMenu.MenuArrowSize", "AtomUI.Token"); public static readonly TokenResourceKey MenuArrowOffset = new TokenResourceKey("NavMenu.MenuArrowOffset", "AtomUI.Token"); diff --git a/src/AtomUI.Controls/NavMenu/NavMenu.cs b/src/AtomUI.Controls/NavMenu/NavMenu.cs index 18f1bbc77..73755a704 100644 --- a/src/AtomUI.Controls/NavMenu/NavMenu.cs +++ b/src/AtomUI.Controls/NavMenu/NavMenu.cs @@ -7,6 +7,7 @@ using Avalonia.Automation; using Avalonia.Automation.Peers; using Avalonia.Controls; using Avalonia.Controls.Metadata; +using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Input; @@ -31,10 +32,17 @@ public class NavMenu : NavMenuBase /// Defines the property. /// public static readonly StyledProperty ModeProperty = - AvaloniaProperty.Register(nameof(Mode), NavMenuMode.Horizontal); + AvaloniaProperty.Register(nameof(Mode), NavMenuMode.Horizontal); public static readonly StyledProperty IsDarkStyleProperty = - AvaloniaProperty.Register(nameof(IsDarkStyle), false); + AvaloniaProperty.Register(nameof(IsDarkStyle), false); + + public static readonly StyledProperty ActiveBarWidthProperty = + AvaloniaProperty.Register(nameof(ActiveBarWidth), 1.0d, + coerce: (o, v) => Math.Max(Math.Min(v, 1.0), 0.0)); + + public static readonly StyledProperty ActiveBarHeightProperty = + AvaloniaProperty.Register(nameof(ActiveBarHeight)); public NavMenuMode Mode { @@ -48,6 +56,18 @@ public class NavMenu : NavMenuBase set => SetValue(IsDarkStyleProperty, value); } + public double ActiveBarWidth + { + get => GetValue(ActiveBarWidthProperty); + set => SetValue(ActiveBarWidthProperty, value); + } + + public double ActiveBarHeight + { + get => GetValue(ActiveBarHeightProperty); + set => SetValue(ActiveBarHeightProperty, value); + } + #endregion #region 内部属性定义 @@ -146,13 +166,18 @@ public class NavMenu : NavMenuBase protected override void PrepareContainerForItemOverride(Control element, object? item, int index) { base.PrepareContainerForItemOverride(element, item, index); - // Child menu items should not inherit the menu's ItemContainerTheme as that is specific // for top-level menu items. if ((element as NavMenuItem)?.ItemContainerTheme == ItemContainerTheme) { element.ClearValue(ItemContainerThemeProperty); } + + if (element is NavMenuItem navMenuItem && Mode == NavMenuMode.Horizontal) + { + BindUtils.RelayBind(this, ActiveBarHeightProperty, navMenuItem, NavMenuItem.ActiveBarHeightProperty); + BindUtils.RelayBind(this, ActiveBarWidthProperty, navMenuItem, NavMenuItem.ActiveBarWidthProperty); + } } private void UpdatePseudoClasses() @@ -178,7 +203,13 @@ public class NavMenu : NavMenuBase BindingPriority.Template, new RenderScaleAwareDoubleConfigure(this)); } - + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + TokenResourceBinder.CreateTokenBinding(this, ActiveBarWidthProperty, NavMenuTokenResourceKey.ActiveBarWidth); + TokenResourceBinder.CreateTokenBinding(this, ActiveBarHeightProperty, NavMenuTokenResourceKey.ActiveBarHeight); + } private void SetupItemContainerTheme(bool force = false) { diff --git a/src/AtomUI.Controls/NavMenu/NavMenuItem.cs b/src/AtomUI.Controls/NavMenu/NavMenuItem.cs index f5abe2569..d72d866fa 100644 --- a/src/AtomUI.Controls/NavMenu/NavMenuItem.cs +++ b/src/AtomUI.Controls/NavMenu/NavMenuItem.cs @@ -208,6 +208,40 @@ public class NavMenuItem : HeaderedSelectingItemsControl, #endregion + #region 内部属性定义 + + internal static readonly StyledProperty ActiveBarWidthProperty = + NavMenu.ActiveBarWidthProperty.AddOwner(); + + internal static readonly StyledProperty ActiveBarHeightProperty = + NavMenu.ActiveBarHeightProperty.AddOwner(); + + internal static readonly DirectProperty EffectiveActiveBarWidthProperty = + AvaloniaProperty.RegisterDirect(nameof(EffectiveActiveBarWidth), + o => o.EffectiveActiveBarWidth, + (o, v) => o.EffectiveActiveBarWidth = v); + + public double ActiveBarWidth + { + get => GetValue(ActiveBarWidthProperty); + set => SetValue(ActiveBarWidthProperty, value); + } + + public double ActiveBarHeight + { + get => GetValue(ActiveBarHeightProperty); + set => SetValue(ActiveBarHeightProperty, value); + } + + private double _effectiveActiveBarWidth; + public double EffectiveActiveBarWidth + { + get => _effectiveActiveBarWidth; + set => SetAndRaise(EffectiveActiveBarWidthProperty, ref _effectiveActiveBarWidth, value); + } + + #endregion + #region 公共事件定义 /// @@ -305,6 +339,7 @@ public class NavMenuItem : HeaderedSelectingItemsControl, private Popup? _popup; private KeyGesture? _hotkey; private bool _isEmbeddedInMenu; + private Border? _horizontalFrame; static NavMenuItem() { @@ -500,6 +535,8 @@ public class NavMenuItem : HeaderedSelectingItemsControl, _popup.Opened += PopupOpened; _popup.Closed += PopupClosed; } + + _horizontalFrame = e.NameScope.Find(TopLevelHorizontalNavMenuItemTheme.FramePart); } protected override void UpdateDataValidation( @@ -643,9 +680,22 @@ public class NavMenuItem : HeaderedSelectingItemsControl, else if (change.Property == CommandParameterProperty) { CommandParameterChanged(change); + } else if (change.Property == BoundsProperty) + { + SetupHorizontalEffectiveIndicatorWidth(); } } + private void SetupHorizontalEffectiveIndicatorWidth() + { + var width = Bounds.Width; + if (_horizontalFrame != null) + { + width = _horizontalFrame.Bounds.Width; + } + EffectiveActiveBarWidth = ActiveBarWidth * width; + } + /// /// Called when the property changes. /// diff --git a/src/AtomUI.Controls/NavMenu/NavMenuToken.cs b/src/AtomUI.Controls/NavMenu/NavMenuToken.cs index ab3122b54..0597d8958 100644 --- a/src/AtomUI.Controls/NavMenu/NavMenuToken.cs +++ b/src/AtomUI.Controls/NavMenu/NavMenuToken.cs @@ -129,27 +129,27 @@ internal class NavMenuToken : AbstractControlDesignToken /// 水平菜单项选中态背景色 /// public Color HorizontalItemSelectedBg { get; set; } - + /// /// 菜单项指示条宽度 /// - public double ActiveBarWidth { get; set; } + public double ActiveBarWidth { get; set; } = double.NaN; /// /// 菜单项指示条高度 /// - public double ActiveBarHeight { get; set; } + public double ActiveBarHeight { get; set; } = double.NaN; /// - /// 菜单项指示条边框宽度 - /// - public double ActiveBarBorderWidth { get; set; } - - /// - /// 菜单项横向外间距 + /// 菜单项外间距 /// public Thickness ItemMargin { get; set; } + /// + /// 水平菜单项外间距 + /// + public Thickness HorizontalItemMargin { get; set; } + /// /// 菜单项横向内间距 /// @@ -275,9 +275,6 @@ internal class NavMenuToken : AbstractControlDesignToken /// 暗色模式下的危险菜单项激活态背景 /// public Color DarkDangerItemActiveBg { get; set; } - - // 内部属性 - public double ItemWidth { get; set; } #region 菜单相关 Token @@ -314,10 +311,10 @@ internal class NavMenuToken : AbstractControlDesignToken var colorTextLightSolid = _globalToken.ColorTextLightSolid; - var activeBarWidth = !double.IsNaN(ActiveBarWidth) ? ActiveBarWidth : 0.0d; - var activeBarBorderWidth = !double.IsNaN(ActiveBarBorderWidth) - ? ActiveBarBorderWidth - : _globalToken.LineWidth; + var activeBarWidth = !double.IsNaN(ActiveBarWidth) ? ActiveBarWidth : 1.0d; + var activeBarHeight = !double.IsNaN(ActiveBarHeight) + ? ActiveBarHeight + : _globalToken.LineWidthBold; var itemMargin = ItemMargin != default ? ItemMargin : new Thickness(_globalToken.MarginXXS, _globalToken.MarginXXS); var colorTextDark = ColorUtils.FromRgbF( 0.65d, @@ -342,8 +339,7 @@ internal class NavMenuToken : AbstractControlDesignToken ItemSelectedBg = _globalToken.ControlItemBgActive; HorizontalItemSelectedBg = Colors.Transparent; ActiveBarWidth = activeBarWidth; - ActiveBarHeight = _globalToken.LineWidthBold; - ActiveBarBorderWidth = activeBarBorderWidth; + ActiveBarHeight = activeBarHeight; // Disabled ItemDisabledColor = _globalToken.ColorTextDisabled; @@ -393,6 +389,7 @@ internal class NavMenuToken : AbstractControlDesignToken DarkDangerItemActiveBg = _globalToken.ColorError; MenuHorizontalHeight = _globalToken.ControlHeightLG * 1.15; + HorizontalItemMargin = new Thickness(_globalToken.Padding, 0); MenuArrowSize = _globalToken.FontSize / 7 * 5; MenuArrowOffset = MenuArrowSize * 0.25; MenuSubMenuBg = _globalToken.ColorBgElevated; diff --git a/src/AtomUI.Controls/NavMenu/TopLevelHorizontalNavMenuItemTheme.cs b/src/AtomUI.Controls/NavMenu/TopLevelHorizontalNavMenuItemTheme.cs index c70ca5180..8ecfccd22 100644 --- a/src/AtomUI.Controls/NavMenu/TopLevelHorizontalNavMenuItemTheme.cs +++ b/src/AtomUI.Controls/NavMenu/TopLevelHorizontalNavMenuItemTheme.cs @@ -1,13 +1,20 @@ -using AtomUI.Theme; +using AtomUI.Icon; +using AtomUI.Media; +using AtomUI.Theme; using AtomUI.Theme.Styling; +using AtomUI.Theme.Utils; using AtomUI.Utils; +using Avalonia.Animation; using Avalonia.Controls; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; +using Avalonia.Controls.Shapes; using Avalonia.Controls.Templates; using Avalonia.Data; +using Avalonia.Data.Converters; using Avalonia.Input; using Avalonia.Layout; +using Avalonia.Media; using Avalonia.Styling; namespace AtomUI.Controls; @@ -21,6 +28,7 @@ internal class TopLevelHorizontalNavMenuItemTheme : BaseControlTheme public const string HeaderPresenterPart = "PART_HeaderPresenter"; public const string ItemsPresenterPart = "PART_ItemsPresenter"; public const string ItemIconPresenterPart = "PART_ItemIconPresenter"; + public const string ActiveIndicatorPart = "PART_ActiveIndicator"; public TopLevelHorizontalNavMenuItemTheme() : base(typeof(NavMenuItem)) { @@ -37,19 +45,24 @@ internal class TopLevelHorizontalNavMenuItemTheme : BaseControlTheme { BuildInstanceStyles(menuItem); + var rootLayout = new Panel(); + var frame = new Border() { - Name = FramePart + Name = FramePart, + HorizontalAlignment = HorizontalAlignment.Stretch, + VerticalAlignment = VerticalAlignment.Stretch }; + frame.RegisterInNameScope(scope); - var rootContainerLayout = new DockPanel() + var contentLayout = new DockPanel() { LastChildFill = true }; var popup = CreateMenuPopup(); popup.RegisterInNameScope(scope); - rootContainerLayout.Children.Add(popup); + contentLayout.Children.Add(popup); var iconPresenter = new ContentPresenter() { @@ -61,10 +74,13 @@ internal class TopLevelHorizontalNavMenuItemTheme : BaseControlTheme DockPanel.SetDock(iconPresenter, Dock.Left); CreateTemplateParentBinding(iconPresenter, ContentPresenter.ContentProperty, NavMenuItem.IconProperty); + CreateTemplateParentBinding(iconPresenter, ContentPresenter.IsVisibleProperty, NavMenuItem.IconProperty, + BindingMode.Default, + ObjectConverters.IsNotNull); TokenResourceBinder.CreateTokenBinding(iconPresenter, Layoutable.MarginProperty, NavMenuTokenResourceKey.IconMargin); - rootContainerLayout.Children.Add(iconPresenter); + contentLayout.Children.Add(iconPresenter); var contentPresenter = new ContentPresenter { @@ -84,13 +100,32 @@ internal class TopLevelHorizontalNavMenuItemTheme : BaseControlTheme CreateTemplateParentBinding(contentPresenter, Layoutable.MinHeightProperty, Layoutable.MinHeightProperty); CreateTemplateParentBinding(contentPresenter, ContentPresenter.FontSizeProperty, TemplatedControl.FontSizeProperty); + CreateTemplateParentBinding(contentPresenter, ContentPresenter.ForegroundProperty, + TemplatedControl.ForegroundProperty); contentPresenter.RegisterInNameScope(scope); - rootContainerLayout.Children.Add(contentPresenter); + contentLayout.Children.Add(contentPresenter); - frame.Child = rootContainerLayout; + frame.Child = contentLayout; - return frame; + rootLayout.Children.Add(frame); + + var activeIndicator = new Rectangle() + { + Name = ActiveIndicatorPart, + VerticalAlignment = VerticalAlignment.Bottom, + HorizontalAlignment = HorizontalAlignment.Center, + Transitions = new Transitions() + { + AnimationUtils.CreateTransition(Rectangle.FillProperty) + } + }; + CreateTemplateParentBinding(activeIndicator, Rectangle.HeightProperty, NavMenuItem.ActiveBarHeightProperty); + CreateTemplateParentBinding(activeIndicator, Rectangle.WidthProperty, NavMenuItem.EffectiveActiveBarWidthProperty); + + rootLayout.Children.Add(activeIndicator); + + return rootLayout; }); } @@ -100,7 +135,7 @@ internal class TopLevelHorizontalNavMenuItemTheme : BaseControlTheme { Name = PopupPart, WindowManagerAddShadowHint = false, - IsLightDismissEnabled = false, + IsLightDismissEnabled = true, Placement = PlacementMode.BottomEdgeAlignedLeft }; @@ -145,45 +180,49 @@ internal class TopLevelHorizontalNavMenuItemTheme : BaseControlTheme } protected override void BuildStyles() - { - var topLevelStyle = new Style(selector => selector.Nesting().Class(NavMenuItem.TopLevelPC)); - topLevelStyle.Add(NavMenuItem.CursorProperty, new Cursor(StandardCursorType.Hand)); - BuildCommonStyle(topLevelStyle); - BuildDisabledStyle(topLevelStyle); - Add(topLevelStyle); - } - - private void BuildCommonStyle(Style topLevelStyle) { var commonStyle = - new Style(selector => selector.Nesting().PropertyEquals(InputElement.IsEnabledProperty, true)); + new Style(selector => selector.Nesting()); commonStyle.Add(TemplatedControl.BackgroundProperty, GlobalTokenResourceKey.ColorTransparent); - - // hover 状态 - var hoverStyle = new Style(selector => selector.Nesting().Class(StdPseudoClass.PointerOver)); - { - } - commonStyle.Add(hoverStyle); - - var frameStyle = new Style(selector => selector.Nesting().Template().Name(FramePart)); - - frameStyle.Add(Border.PaddingProperty, NavMenuTokenResourceKey.ItemPadding); - commonStyle.Add(frameStyle); - + commonStyle.Add(NavMenuItem.CursorProperty, new Cursor(StandardCursorType.Hand)); commonStyle.Add(TemplatedControl.FontSizeProperty, GlobalTokenResourceKey.FontSize); + { + var frameStyle = new Style(selector => selector.Nesting().Template().Name(FramePart)); + frameStyle.Add(Border.MarginProperty, NavMenuTokenResourceKey.HorizontalItemMargin); + commonStyle.Add(frameStyle); + } var presenterStyle = new Style(selector => selector.Nesting().Template().Name(HeaderPresenterPart)); presenterStyle.Add(ContentPresenter.LineHeightProperty, NavMenuTokenResourceKey.HorizontalLineHeight); commonStyle.Add(presenterStyle); - topLevelStyle.Add(commonStyle); + BuildActiveIndicatorStyle(commonStyle); + Add(commonStyle); + BuildDisabledStyle(); } - private void BuildDisabledStyle(Style topLevelStyle) + private void BuildActiveIndicatorStyle(Style commonStyle) + { + { + var indicatorStyle = new Style(selector => selector.Nesting().Template().Name(ActiveIndicatorPart)); + indicatorStyle.Add(Rectangle.FillProperty, new SolidColorBrush(Colors.Transparent)); + commonStyle.Add(indicatorStyle); + } + var hoverStyle = new Style(selector => Selectors.Or(selector.Nesting().Class(StdPseudoClass.PointerOver), + selector.Nesting().Class(StdPseudoClass.Open))); + { + var indicatorStyle = new Style(selector => selector.Nesting().Template().Name(ActiveIndicatorPart)); + indicatorStyle.Add(Rectangle.FillProperty, GlobalTokenResourceKey.ColorPrimary); + hoverStyle.Add(indicatorStyle); + } + commonStyle.Add(hoverStyle); + } + + private void BuildDisabledStyle() { var disabledStyle = new Style(selector => selector.Nesting().Class(StdPseudoClass.Disabled)); disabledStyle.Add(TemplatedControl.ForegroundProperty, NavMenuTokenResourceKey.ItemDisabledColor); - topLevelStyle.Add(disabledStyle); + Add(disabledStyle); } protected override void BuildInstanceStyles(Control control) @@ -192,6 +231,12 @@ internal class TopLevelHorizontalNavMenuItemTheme : BaseControlTheme iconStyle.Add(PathIcon.WidthProperty, NavMenuTokenResourceKey.ItemIconSize); iconStyle.Add(PathIcon.HeightProperty, NavMenuTokenResourceKey.ItemIconSize); iconStyle.Add(PathIcon.NormalFilledBrushProperty, GlobalTokenResourceKey.ColorText); + iconStyle.Add(PathIcon.DisabledFilledBrushProperty, NavMenuTokenResourceKey.ItemDisabledColor); + iconStyle.Add(PathIcon.SelectedFilledBrushProperty, GlobalTokenResourceKey.ColorPrimary); control.Styles.Add(iconStyle); + + var disabledIconStyle = new Style(selector => selector.OfType().Class(StdPseudoClass.Disabled)); + disabledIconStyle.Add(PathIcon.IconModeProperty, IconMode.Disabled); + control.Styles.Add(disabledIconStyle); } } \ No newline at end of file