diff --git a/README.md b/README.md index cd3ce8b..8f8500e 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,7 @@ Welcome to communicate and give suggestions to AtomUI, thank you for giving the | Breadcrumb | TODO | | Dropdown | Completed ✅ | | Menu | Completed ✅ | +| NavMenu | Completed ✅ | | Pagination | TODO | | Steps | TODO | diff --git a/README.zh-CN.md b/README.zh-CN.md index 2f853bc..9cb0189 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -90,7 +90,8 @@ PS: AtomUI 目前仅在 Windows 11 平台测试
|:---------------|:-------| | Breadcrumb 面包屑 | 未完成 | | Dropdown 下拉菜单 | 已完成 ✅ | -| Menu 导航菜单 | 已完成 ✅ | +| Menu 菜单 | 已完成 ✅ | +| NavMenu 导航菜单 | 已完成 ✅ | | Pagination 分页 | 进行中 💪 | | Steps 步骤条 | 未完成 | diff --git a/samples/AtomUI.Demo.Desktop/ShowCase/MenuShowCase.axaml b/samples/AtomUI.Demo.Desktop/ShowCase/MenuShowCase.axaml index 801bea6..258b811 100644 --- a/samples/AtomUI.Demo.Desktop/ShowCase/MenuShowCase.axaml +++ b/samples/AtomUI.Demo.Desktop/ShowCase/MenuShowCase.axaml @@ -241,7 +241,7 @@ - + @@ -250,6 +250,36 @@ + + + + + + Change Mode + + Change Style + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/AtomUI.Demo.Desktop/ShowCase/MenuShowCase.axaml.cs b/samples/AtomUI.Demo.Desktop/ShowCase/MenuShowCase.axaml.cs index ef812aa..8b3e53b 100644 --- a/samples/AtomUI.Demo.Desktop/ShowCase/MenuShowCase.axaml.cs +++ b/samples/AtomUI.Demo.Desktop/ShowCase/MenuShowCase.axaml.cs @@ -1,11 +1,66 @@ +using AtomUI.Controls; +using Avalonia; using Avalonia.Controls; +using Avalonia.Interactivity; namespace AtomUI.Demo.Desktop.ShowCase; public partial class MenuShowCase : UserControl { + public static readonly StyledProperty IsDarkProperty = + AvaloniaProperty.Register(nameof(IsDark), false); + + public static readonly StyledProperty ModeProperty = + AvaloniaProperty.Register(nameof(Mode), NavMenuMode.Inline); + + public bool IsDark + { + get => GetValue(IsDarkProperty); + set => SetValue(IsDarkProperty, value); + } + + public NavMenuMode Mode + { + get => GetValue(ModeProperty); + set => SetValue(ModeProperty, value); + } + public MenuShowCase() { InitializeComponent(); + DataContext = this; + ChangeModeSwitch.IsCheckedChanged += HandleChangeModeCheckChanged; + ChangeStyleSwitch.IsCheckedChanged += HandleChangeStyleCheckChanged; + } + + private void HandleChangeModeCheckChanged(object? sender, RoutedEventArgs? args) + { + if (ChangeModeSwitch.IsChecked.HasValue) + { + if (ChangeModeSwitch.IsChecked.Value) + { + Mode = NavMenuMode.Vertical; + } + else + { + Mode = NavMenuMode.Inline; + } + } + else + { + Mode = NavMenuMode.Inline; + } + } + + private void HandleChangeStyleCheckChanged(object? sender, RoutedEventArgs? args) + { + if (ChangeStyleSwitch.IsChecked.HasValue) + { + IsDark = ChangeStyleSwitch.IsChecked.Value; + } + else + { + IsDark = false; + } } } \ 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 2790c34..4a0dbed 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 @@ -406,6 +406,7 @@ namespace AtomUI.Theme.Styling public static readonly TokenResourceKey ItemContentMargin = new TokenResourceKey("NavMenu.ItemContentMargin", "AtomUI.Token"); public static readonly TokenResourceKey ItemContentPadding = new TokenResourceKey("NavMenu.ItemContentPadding", "AtomUI.Token"); public static readonly TokenResourceKey VerticalItemsPanelSpacing = new TokenResourceKey("NavMenu.VerticalItemsPanelSpacing", "AtomUI.Token"); + public static readonly TokenResourceKey VerticalMenuContentPadding = new TokenResourceKey("NavMenu.VerticalMenuContentPadding", "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 HorizontalItemHoverBg = new TokenResourceKey("NavMenu.HorizontalItemHoverBg", "AtomUI.Token"); diff --git a/src/AtomUI.Controls/NavMenu/BaseNavMenuItemTheme.cs b/src/AtomUI.Controls/NavMenu/BaseNavMenuItemTheme.cs index c7a4a49..ede3232 100644 --- a/src/AtomUI.Controls/NavMenu/BaseNavMenuItemTheme.cs +++ b/src/AtomUI.Controls/NavMenu/BaseNavMenuItemTheme.cs @@ -20,7 +20,7 @@ namespace AtomUI.Controls; internal class BaseNavMenuItemTheme : BaseControlTheme { - public const string ItemDecoratorPart = "PART_ItemDecorator"; + public const string HeaderDecoratorPart = "PART_HeaderDecorator"; public const string MainContainerPart = "PART_MainContainer"; public const string ItemIconPresenterPart = "PART_ItemIconPresenter"; public const string ItemTextPresenterPart = "PART_ItemTextPresenter"; @@ -35,7 +35,6 @@ internal class BaseNavMenuItemTheme : BaseControlTheme { return new FuncControlTemplate((item, scope) => { - BuildInstanceStyles(item); // 仅仅为了把 Popup 包进来,没有其他什么作用 var layoutWrapper = new Panel(); var header = BuildMenuItemContent(item, scope); @@ -49,13 +48,14 @@ internal class BaseNavMenuItemTheme : BaseControlTheme { var headerFrame = new Border { - Name = ItemDecoratorPart + Name = HeaderDecoratorPart, + Transitions = new Transitions() + { + AnimationUtils.CreateTransition(Border.BackgroundProperty), + AnimationUtils.CreateTransition(TemplatedControl.ForegroundProperty) + } }; - - var transitions = new Transitions(); - transitions.Add(AnimationUtils.CreateTransition(Border.BackgroundProperty)); - headerFrame.Transitions = transitions; - + headerFrame.RegisterInNameScope(scope); headerFrame.Child = BuildMenuItemInfoGrid(navMenuItem, scope); return headerFrame; } @@ -84,17 +84,16 @@ internal class BaseNavMenuItemTheme : BaseControlTheme }; layout.RegisterInNameScope(scope); - var iconPresenter = new Viewbox + var iconPresenter = new ContentPresenter() { Name = ItemIconPresenterPart, HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center, - Stretch = Stretch.Uniform }; Grid.SetColumn(iconPresenter, 0); iconPresenter.RegisterInNameScope(scope); - CreateTemplateParentBinding(iconPresenter, Viewbox.ChildProperty, NavMenuItem.IconProperty); + CreateTemplateParentBinding(iconPresenter, ContentPresenter.ContentProperty, NavMenuItem.IconProperty); TokenResourceBinder.CreateTokenBinding(iconPresenter, Layoutable.MarginProperty, NavMenuTokenResourceKey.ItemMargin); TokenResourceBinder.CreateGlobalTokenBinding(iconPresenter, Layoutable.WidthProperty, @@ -161,17 +160,9 @@ internal class BaseNavMenuItemTheme : BaseControlTheme VerticalAlignment = VerticalAlignment.Center, Kind = "RightOutlined" }; - CreateTemplateParentBinding(menuIndicatorIcon, PathIcon.IsEnabledProperty, NavMenuItem.IsEnabledProperty); - TokenResourceBinder.CreateGlobalTokenBinding(menuIndicatorIcon, PathIcon.NormalFilledBrushProperty, - NavMenuTokenResourceKey.ItemColor); - TokenResourceBinder.CreateGlobalTokenBinding(menuIndicatorIcon, PathIcon.SelectedFilledBrushProperty, - NavMenuTokenResourceKey.ItemSelectedColor); - TokenResourceBinder.CreateGlobalTokenBinding(menuIndicatorIcon, PathIcon.DisabledFilledBrushProperty, - NavMenuTokenResourceKey.ItemDisabledColor); - TokenResourceBinder.CreateGlobalTokenBinding(menuIndicatorIcon, Layoutable.WidthProperty, NavMenuTokenResourceKey.MenuArrowSize); TokenResourceBinder.CreateGlobalTokenBinding(menuIndicatorIcon, Layoutable.HeightProperty, @@ -199,17 +190,17 @@ internal class BaseNavMenuItemTheme : BaseControlTheme commonStyle.Add(keyGestureStyle); } { - var borderStyle = new Style(selector => selector.Nesting().Template().Name(ItemDecoratorPart)); + var borderStyle = new Style(selector => selector.Nesting().Template().Name(HeaderDecoratorPart)); borderStyle.Add(Border.CursorProperty, new Cursor(StandardCursorType.Hand)); - borderStyle.Add(Layoutable.MinHeightProperty, NavMenuTokenResourceKey.ItemHeight); - borderStyle.Add(Decorator.PaddingProperty, NavMenuTokenResourceKey.ItemContentPadding); + borderStyle.Add(Border.MinHeightProperty, NavMenuTokenResourceKey.ItemHeight); + borderStyle.Add(Border.PaddingProperty, NavMenuTokenResourceKey.ItemContentPadding); borderStyle.Add(Border.BackgroundProperty, NavMenuTokenResourceKey.ItemBg); borderStyle.Add(Border.CornerRadiusProperty, NavMenuTokenResourceKey.ItemBorderRadius); commonStyle.Add(borderStyle); } // Hover 状态 - var hoverStyle = new Style(selector => selector.Nesting().Template().Name(ItemDecoratorPart).Class(StdPseudoClass.PointerOver)); + var hoverStyle = new Style(selector => selector.Nesting().Template().Name(HeaderDecoratorPart).Class(StdPseudoClass.PointerOver)); hoverStyle.Add(TemplatedControl.ForegroundProperty, NavMenuTokenResourceKey.ItemHoverColor); hoverStyle.Add(Border.BackgroundProperty, NavMenuTokenResourceKey.ItemHoverBg); commonStyle.Add(hoverStyle); @@ -219,7 +210,7 @@ internal class BaseNavMenuItemTheme : BaseControlTheme { var selectedStyle = new Style(selector => selector.Nesting().Class(StdPseudoClass.Selected)); { - var itemDecoratorStyle = new Style(selector => selector.Nesting().Template().Name(ItemDecoratorPart)); + var itemDecoratorStyle = new Style(selector => selector.Nesting().Template().Name(HeaderDecoratorPart)); itemDecoratorStyle.Add(Border.BackgroundProperty, NavMenuTokenResourceKey.ItemSelectedBg); itemDecoratorStyle.Add(TemplatedControl.ForegroundProperty, NavMenuTokenResourceKey.ItemSelectedColor); selectedStyle.Add(itemDecoratorStyle); @@ -232,7 +223,7 @@ internal class BaseNavMenuItemTheme : BaseControlTheme { var selectedStyle = new Style(selector => selector.Nesting().Class(StdPseudoClass.Selected)); { - var itemDecoratorStyle = new Style(selector => selector.Nesting().Template().Name(ItemDecoratorPart)); + var itemDecoratorStyle = new Style(selector => selector.Nesting().Template().Name(HeaderDecoratorPart)); itemDecoratorStyle.Add(TemplatedControl.ForegroundProperty, NavMenuTokenResourceKey.ItemSelectedColor); selectedStyle.Add(itemDecoratorStyle); } @@ -240,25 +231,94 @@ internal class BaseNavMenuItemTheme : BaseControlTheme } commonStyle.Add(hasSubMenuStyle); Add(commonStyle); + + BuildDarkCommonStyle(); + } + + private void BuildDarkCommonStyle() + { + var darkCommonStyle = new Style(selector => selector.Nesting().PropertyEquals(NavMenuItem.IsDarkStyleProperty, true)); + darkCommonStyle.Add(NavMenuItem.ForegroundProperty, NavMenuTokenResourceKey.DarkItemColor); + + { + var borderStyle = new Style(selector => selector.Nesting().Template().Name(HeaderDecoratorPart)); + borderStyle.Add(Border.BackgroundProperty, NavMenuTokenResourceKey.DarkItemBg); + darkCommonStyle.Add(borderStyle); + } + + // 选中分两种,一种是有子菜单一种是没有子菜单 + var hasNoSubMenuStyle = new Style(selector => selector.Nesting().PropertyEquals(NavMenuItem.HasSubMenuProperty, false)); + { + // Hover 状态 + var hoverStyle = new Style(selector => selector.Nesting().Template().Name(HeaderDecoratorPart).Class(StdPseudoClass.PointerOver)); + hoverStyle.Add(TemplatedControl.ForegroundProperty, NavMenuTokenResourceKey.DarkItemHoverColor); + hoverStyle.Add(Border.BackgroundProperty, NavMenuTokenResourceKey.DarkItemHoverBg); + hasNoSubMenuStyle.Add(hoverStyle); + + var selectedStyle = new Style(selector => selector.Nesting().Class(StdPseudoClass.Selected)); + { + var itemDecoratorStyle = new Style(selector => selector.Nesting().Template().Name(HeaderDecoratorPart)); + itemDecoratorStyle.Add(Border.BackgroundProperty, NavMenuTokenResourceKey.DarkItemSelectedBg); + itemDecoratorStyle.Add(TemplatedControl.ForegroundProperty, NavMenuTokenResourceKey.DarkItemSelectedColor); + selectedStyle.Add(itemDecoratorStyle); + } + hasNoSubMenuStyle.Add(selectedStyle); + } + darkCommonStyle.Add(hasNoSubMenuStyle); + + var hasSubMenuStyle = new Style(selector => selector.Nesting().PropertyEquals(NavMenuItem.HasSubMenuProperty, true)); + { + // Hover 状态 + var hoverStyle = new Style(selector => selector.Nesting().Template().Name(HeaderDecoratorPart).Class(StdPseudoClass.PointerOver)); + hoverStyle.Add(TemplatedControl.ForegroundProperty, NavMenuTokenResourceKey.DarkItemColor); + hoverStyle.Add(Border.BackgroundProperty, NavMenuTokenResourceKey.DarkItemBg); + hasSubMenuStyle.Add(hoverStyle); + + var selectedStyle = new Style(selector => selector.Nesting().Class(StdPseudoClass.Selected)); + { + var itemDecoratorStyle = new Style(selector => selector.Nesting().Template().Name(HeaderDecoratorPart)); + itemDecoratorStyle.Add(TemplatedControl.ForegroundProperty, NavMenuTokenResourceKey.DarkItemSelectedColor); + selectedStyle.Add(itemDecoratorStyle); + } + hasSubMenuStyle.Add(selectedStyle); + } + darkCommonStyle.Add(hasSubMenuStyle); + + Add(darkCommonStyle); } private void BuildMenuIndicatorStyle() { { - { - var menuIndicatorStyle = new Style(selector => selector.Nesting().Template().Name(MenuIndicatorIconPart)); - menuIndicatorStyle.Add(Visual.IsVisibleProperty, true); - Add(menuIndicatorStyle); - } + var menuIndicatorStyle = new Style(selector => selector.Nesting().Template().Name(MenuIndicatorIconPart)); + menuIndicatorStyle.Add(Visual.IsVisibleProperty, true); + menuIndicatorStyle.Add(PathIcon.NormalFilledBrushProperty, NavMenuTokenResourceKey.ItemColor); + menuIndicatorStyle.Add(PathIcon.SelectedFilledBrushProperty, NavMenuTokenResourceKey.ItemSelectedColor); + menuIndicatorStyle.Add(PathIcon.DisabledFilledBrushProperty, NavMenuTokenResourceKey.ItemDisabledColor); + // 设置颜色 - var selectedStyle = new Style(selector => selector.Nesting().Class(StdPseudoClass.Selected)); + Add(menuIndicatorStyle); + } + { + var darkCommonStyle = new Style(selector => selector.Nesting().PropertyEquals(NavMenuItem.IsDarkStyleProperty, true)); { var menuIndicatorStyle = new Style(selector => selector.Nesting().Template().Name(MenuIndicatorIconPart)); - menuIndicatorStyle.Add(PathIcon.IconModeProperty, IconMode.Selected); - selectedStyle.Add(menuIndicatorStyle); + menuIndicatorStyle.Add(PathIcon.NormalFilledBrushProperty, NavMenuTokenResourceKey.DarkItemColor); + menuIndicatorStyle.Add(PathIcon.SelectedFilledBrushProperty, NavMenuTokenResourceKey.DarkItemSelectedColor); + menuIndicatorStyle.Add(PathIcon.DisabledFilledBrushProperty, NavMenuTokenResourceKey.DarkItemDisabledColor); + darkCommonStyle.Add(menuIndicatorStyle); } - Add(selectedStyle); + Add(darkCommonStyle); } + + var selectedStyle = new Style(selector => selector.Nesting().Class(StdPseudoClass.Selected)); + { + var menuIndicatorStyle = new Style(selector => selector.Nesting().Template().Name(MenuIndicatorIconPart)); + menuIndicatorStyle.Add(PathIcon.IconModeProperty, IconMode.Selected); + selectedStyle.Add(menuIndicatorStyle); + } + Add(selectedStyle); + var hasNoSubMenuStyle = new Style(selector => selector.Nesting().PropertyEquals(NavMenuItem.HasSubMenuProperty, false)); { var menuIndicatorStyle = new Style(selector => selector.Nesting().Template().Name(MenuIndicatorIconPart)); @@ -271,41 +331,34 @@ internal class BaseNavMenuItemTheme : BaseControlTheme private void BuildMenuIconStyle() { { - var iconViewBoxStyle = new Style(selector => selector.Nesting().Template().Name(ItemIconPresenterPart)); - iconViewBoxStyle.Add(Visual.IsVisibleProperty, false); - Add(iconViewBoxStyle); + var iconContentPresenterStyle = new Style(selector => selector.Nesting().Template().Name(ItemIconPresenterPart)); + iconContentPresenterStyle.Add(Visual.IsVisibleProperty, false); + Add(iconContentPresenterStyle); } var hasIconStyle = new Style(selector => selector.Nesting().Class(":icon")); { - var iconViewBoxStyle = new Style(selector => selector.Nesting().Template().Name(ItemIconPresenterPart)); - iconViewBoxStyle.Add(Visual.IsVisibleProperty, true); - hasIconStyle.Add(iconViewBoxStyle); + var iconContentPresenterStyle = new Style(selector => selector.Nesting().Template().Name(ItemIconPresenterPart)); + iconContentPresenterStyle.Add(Visual.IsVisibleProperty, true); + hasIconStyle.Add(iconContentPresenterStyle); } Add(hasIconStyle); } private void BuildDisabledStyle() { - var disabledStyle = new Style(selector => selector.Nesting().Class(StdPseudoClass.Disabled)); - disabledStyle.Add(TemplatedControl.ForegroundProperty, NavMenuTokenResourceKey.ItemDisabledColor); - Add(disabledStyle); + { + var disabledStyle = new Style(selector => selector.Nesting().Class(StdPseudoClass.Disabled)); + disabledStyle.Add(TemplatedControl.ForegroundProperty, NavMenuTokenResourceKey.ItemDisabledColor); + Add(disabledStyle); + } + var darkStyle = new Style(selector => selector.Nesting().PropertyEquals(NavMenuItem.IsDarkStyleProperty, true)); + { + var disabledStyle = new Style(selector => selector.Nesting().Class(StdPseudoClass.Disabled)); + disabledStyle.Add(TemplatedControl.ForegroundProperty, NavMenuTokenResourceKey.DarkItemDisabledColor); + darkStyle.Add(disabledStyle); + } + Add(darkStyle); } - protected override void BuildInstanceStyles(Control control) - { - { - var iconStyle = new Style(selector => selector.Name(ThemeConstants.ItemIconPart)); - 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 diff --git a/src/AtomUI.Controls/NavMenu/InlineNavMenuItemTheme.cs b/src/AtomUI.Controls/NavMenu/InlineNavMenuItemTheme.cs index 5155574..6f0d313 100644 --- a/src/AtomUI.Controls/NavMenu/InlineNavMenuItemTheme.cs +++ b/src/AtomUI.Controls/NavMenu/InlineNavMenuItemTheme.cs @@ -56,20 +56,21 @@ internal class InlineNavMenuItemTheme : BaseNavMenuItemTheme var headerContent = base.BuildMenuItemContent(navMenuItem, scope); - TokenResourceBinder.CreateTokenBinding(headerContent, Control.MarginProperty, NavMenuTokenResourceKey.VerticalItemsPanelSpacing, BindingPriority.Template, - (v) => - { - if (v is double dval) - { - return new Thickness(0, 0, 0, dval); - } - - return new Thickness(); - }); var childItemsLayoutTransform = new LayoutTransformControl() { Name = ChildItemsLayoutTransformPart, }; + TokenResourceBinder.CreateTokenBinding(childItemsLayoutTransform, LayoutTransformControl.MarginProperty, + NavMenuTokenResourceKey.VerticalItemsPanelSpacing, BindingPriority.Template, + (v) => + { + if (v is double dval) + { + return new Thickness(0, dval, 0, 0); + } + + return new Thickness(); + }); childItemsLayoutTransform.RegisterInNameScope(scope); var itemsPresenter = new ItemsPresenter @@ -106,6 +107,10 @@ internal class InlineNavMenuItemTheme : BaseNavMenuItemTheme { base.BuildStyles(); BuildMenuIndicatorStyle(); + + var itemsPanelStyle = new Style(selector => selector.Nesting().Template().Name(ChildItemsPresenterPart).Child().OfType()); + itemsPanelStyle.Add(StackPanel.SpacingProperty, NavMenuTokenResourceKey.VerticalItemsPanelSpacing); + Add(itemsPanelStyle); } private void BuildMenuIndicatorStyle() diff --git a/src/AtomUI.Controls/NavMenu/NavMenu.cs b/src/AtomUI.Controls/NavMenu/NavMenu.cs index 48335ab..5c94887 100644 --- a/src/AtomUI.Controls/NavMenu/NavMenu.cs +++ b/src/AtomUI.Controls/NavMenu/NavMenu.cs @@ -147,13 +147,20 @@ public class NavMenu : NavMenuBase { if (change.Property == ModeProperty) { - SetupItemContainerTheme(true); - SetupInteractionHandler(true); + HandleModeChanged(); } UpdatePseudoClasses(); } } + private void HandleModeChanged() + { + CloseChildItemsRecursively(); + SetupItemContainerTheme(true); + RegenerateContainersRecursively(); + SetupInteractionHandler(true); + } + protected override void PrepareContainerForItemOverride(Control element, object? item, int index) { base.PrepareContainerForItemOverride(element, item, index); @@ -172,7 +179,9 @@ public class NavMenu : NavMenuBase BindUtils.RelayBind(this, ActiveBarWidthProperty, navMenuItem, NavMenuItem.ActiveBarWidthProperty); } BindUtils.RelayBind(this, ModeProperty, navMenuItem, NavMenuItem.ModeProperty); + BindUtils.RelayBind(this, IsDarkStyleProperty, navMenuItem, NavMenuItem.IsDarkStyleProperty); } + } private void UpdatePseudoClasses() @@ -276,4 +285,48 @@ public class NavMenu : NavMenuBase } } } + + private void CloseChildItemsRecursively() + { + CloseInlineItems(); + foreach (var i in ((INavMenu)this).SubItems) + { + i.Close(); + } + } + + private void RegenerateContainersRecursively() + { + foreach (var item in Items) + { + if (item is NavMenuItem navMenuItem) + { + navMenuItem.RegenerateContainers(); + } + } + } + + private void CloseInlineItems() + { + foreach (var item in Items) + { + if (item is NavMenuItem navMenuItem) + { + CloseInlineItemRecursively(navMenuItem); + } + } + } + + private void CloseInlineItemRecursively(NavMenuItem navMenuItem) + { + foreach (var item in navMenuItem.Items) + { + if (item is NavMenuItem childNavMenuItem) + { + CloseInlineItemRecursively(childNavMenuItem); + } + } + navMenuItem.CloseInlineItem(); + navMenuItem.IsSubMenuOpen = false; + } } \ No newline at end of file diff --git a/src/AtomUI.Controls/NavMenu/NavMenuItem.cs b/src/AtomUI.Controls/NavMenu/NavMenuItem.cs index 2a60a77..011590b 100644 --- a/src/AtomUI.Controls/NavMenu/NavMenuItem.cs +++ b/src/AtomUI.Controls/NavMenu/NavMenuItem.cs @@ -61,8 +61,8 @@ public class NavMenuItem : HeaderedSelectingItemsControl, /// /// Defines the property. /// - public static readonly StyledProperty IconProperty = - AvaloniaProperty.Register(nameof(Icon)); + public static readonly StyledProperty IconProperty = + AvaloniaProperty.Register(nameof(Icon)); /// /// Defines the property. @@ -71,7 +71,7 @@ public class NavMenuItem : HeaderedSelectingItemsControl, AvaloniaProperty.Register(nameof(InputGesture)); /// - /// Defines the property. + /// Defines the property. /// public static readonly StyledProperty IsSubMenuOpenProperty = AvaloniaProperty.Register(nameof(IsSubMenuOpen)); @@ -126,7 +126,7 @@ public class NavMenuItem : HeaderedSelectingItemsControl, /// /// Gets or sets the icon that appears in a . /// - public object? Icon + public PathIcon? Icon { get => GetValue(IconProperty); set => SetValue(IconProperty, value); @@ -285,6 +285,11 @@ public class NavMenuItem : HeaderedSelectingItemsControl, o => o.InlineItemIndentUnit, (o, v) => o.InlineItemIndentUnit = v); + internal static readonly DirectProperty IsDarkStyleProperty = + AvaloniaProperty.RegisterDirect(nameof(IsDarkStyle), + o => o.IsDarkStyle, + (o, v) => o.IsDarkStyle = v); + internal double ActiveBarWidth { get => GetValue(ActiveBarWidthProperty); @@ -340,6 +345,14 @@ public class NavMenuItem : HeaderedSelectingItemsControl, set => SetAndRaise(InlineItemIndentUnitProperty, ref _inlineItemIndentUnit, value); } + private bool _isDarkStyle; + + internal bool IsDarkStyle + { + get => _isDarkStyle; + set => SetAndRaise(IsDarkStyleProperty, ref _isDarkStyle, value); + } + #endregion #region 公共事件定义 @@ -444,6 +457,7 @@ public class NavMenuItem : HeaderedSelectingItemsControl, private Border? _horizontalFrame; private IDisposable? _itemContainerThemeDisposable; private LayoutTransformControl? _childItemsLayoutTransform; + private Border? _headerFrame; private bool _animating = false; static NavMenuItem() @@ -639,7 +653,13 @@ public class NavMenuItem : HeaderedSelectingItemsControl, _popup.DependencyResolver = null; } - _popup = e.NameScope.Find("PART_Popup"); + _popup = e.NameScope.Find(ThemeConstants.PopupPart); + _headerFrame = e.NameScope.Find(BaseNavMenuItemTheme.HeaderDecoratorPart); + if (_headerFrame is not null) + { + _headerFrame.PointerEntered += HandleHeaderFrameEnter; + _headerFrame.PointerExited += HandleHeaderFrameExit; + } if (_popup != null) { @@ -670,6 +690,34 @@ public class NavMenuItem : HeaderedSelectingItemsControl, } } + private void HandleHeaderFrameEnter(object? sender, PointerEventArgs args) + { + if (!IsDarkStyle || Icon is null || HasSubMenu) + { + return; + } + + Icon.IconMode = IconMode.Active; + } + + private void HandleHeaderFrameExit(object? sender, PointerEventArgs args) + { + if (!IsDarkStyle || Icon is null || HasSubMenu) + { + return; + } + + if (IsSelected) + { + Icon.IconMode = IconMode.Selected; + } + else + { + Icon.IconMode = IconMode.Normal; + } + + } + protected override void UpdateDataValidation( AvaloniaProperty property, BindingValueType state, @@ -816,10 +864,7 @@ public class NavMenuItem : HeaderedSelectingItemsControl, { SetupHorizontalEffectiveIndicatorWidth(); } - else if (change.Property == IconProperty) - { - SetupItemIcon(); - } else if (change.Property == ModeProperty) + else if (change.Property == ModeProperty) { SetupItemContainerTheme(true); } else if (change.Property == ItemCountProperty) @@ -832,14 +877,36 @@ public class NavMenuItem : HeaderedSelectingItemsControl, { SetupEffectivePopupMinWidth(); } - + + if (change.Property == IconProperty || + change.Property == IsDarkStyleProperty) + { + SetupItemIcon(); + } } private void SetupItemIcon() { if (Icon is not null && Icon is PathIcon menuItemIcon) { - menuItemIcon.Name = ThemeConstants.ItemIconPart; + BindUtils.RelayBind(this, IsEnabledProperty, menuItemIcon, PathIcon.IsEnabledProperty); + TokenResourceBinder.CreateTokenBinding(menuItemIcon, PathIcon.WidthProperty, NavMenuTokenResourceKey.ItemIconSize); + TokenResourceBinder.CreateTokenBinding(menuItemIcon, PathIcon.HeightProperty, NavMenuTokenResourceKey.ItemIconSize); + + if (IsDarkStyle) + { + TokenResourceBinder.CreateTokenBinding(menuItemIcon, PathIcon.NormalFilledBrushProperty, NavMenuTokenResourceKey.DarkItemColor); + TokenResourceBinder.CreateTokenBinding(menuItemIcon, PathIcon.SelectedFilledBrushProperty, NavMenuTokenResourceKey.DarkItemSelectedColor); + TokenResourceBinder.CreateTokenBinding(menuItemIcon, PathIcon.ActiveFilledBrushProperty, NavMenuTokenResourceKey.DarkItemHoverColor); + TokenResourceBinder.CreateTokenBinding(menuItemIcon, PathIcon.DisabledFilledBrushProperty, NavMenuTokenResourceKey.DarkItemDisabledColor); + } + else + { + TokenResourceBinder.CreateTokenBinding(menuItemIcon, PathIcon.NormalFilledBrushProperty, NavMenuTokenResourceKey.ItemColor); + TokenResourceBinder.CreateTokenBinding(menuItemIcon, PathIcon.SelectedFilledBrushProperty, NavMenuTokenResourceKey.ItemSelectedColor); + TokenResourceBinder.CreateTokenBinding(menuItemIcon, PathIcon.ActiveFilledBrushProperty, NavMenuTokenResourceKey.ItemHoverColor); + TokenResourceBinder.CreateTokenBinding(menuItemIcon, PathIcon.DisabledFilledBrushProperty, NavMenuTokenResourceKey.ItemDisabledColor); + } } } @@ -901,7 +968,7 @@ public class NavMenuItem : HeaderedSelectingItemsControl, /// The property change event. private void IconChanged(AvaloniaPropertyChangedEventArgs e) { - var (oldValue, newValue) = e.GetOldAndNewValue(); + var (oldValue, newValue) = e.GetOldAndNewValue(); if (oldValue is ILogical oldLogical) { @@ -988,7 +1055,7 @@ public class NavMenuItem : HeaderedSelectingItemsControl, } } - private void OpenInlineItem() + internal void OpenInlineItem() { if (_childItemsLayoutTransform is not null) { @@ -1011,7 +1078,7 @@ public class NavMenuItem : HeaderedSelectingItemsControl, } } - private void CloseInlineItem() + internal void CloseInlineItem() { if (_childItemsLayoutTransform is not null) { @@ -1030,6 +1097,7 @@ public class NavMenuItem : HeaderedSelectingItemsControl, _animating = false; }); } + } /// @@ -1116,6 +1184,7 @@ public class NavMenuItem : HeaderedSelectingItemsControl, if (element is NavMenuItem navMenuItem) { BindUtils.RelayBind(this, ModeProperty, navMenuItem, ModeProperty); + BindUtils.RelayBind(this, IsDarkStyleProperty, navMenuItem, IsDarkStyleProperty); } } @@ -1130,6 +1199,20 @@ public class NavMenuItem : HeaderedSelectingItemsControl, } } } + + internal void RegenerateContainers() + { + foreach (var item in Items) + { + if (item is NavMenuItem childNavMenuItem) + { + childNavMenuItem.RegenerateContainers(); + } + } + + ItemsPanel = new FuncTemplate(() => new StackPanel()); + RefreshContainers(); + } private static int CalculateDistanceFromLogicalParent(ILogical? logical, int @default = -1) where T : class { @@ -1143,4 +1226,5 @@ public class NavMenuItem : HeaderedSelectingItemsControl, return logical != null ? result : @default; } + } \ No newline at end of file diff --git a/src/AtomUI.Controls/NavMenu/NavMenuItemTheme.cs b/src/AtomUI.Controls/NavMenu/NavMenuItemTheme.cs index 268f8ac..a9d4726 100644 --- a/src/AtomUI.Controls/NavMenu/NavMenuItemTheme.cs +++ b/src/AtomUI.Controls/NavMenu/NavMenuItemTheme.cs @@ -13,6 +13,7 @@ namespace AtomUI.Controls; internal class NavMenuItemTheme : BaseNavMenuItemTheme { public const string ItemsPresenterPart = "PART_ItemsPresenter"; + public const string PopupFramePart = "PART_PopupFrame"; public NavMenuItemTheme() : base(typeof(NavMenuItem)) @@ -37,12 +38,16 @@ internal class NavMenuItemTheme : BaseNavMenuItemTheme Name = ThemeConstants.PopupPart, WindowManagerAddShadowHint = false, IsLightDismissEnabled = false, - Placement = PlacementMode.RightEdgeAlignedTop + Placement = PlacementMode.RightEdgeAlignedTop, }; - var border = new Border(); - TokenResourceBinder.CreateTokenBinding(border, Border.BackgroundProperty, - GlobalTokenResourceKey.ColorBgContainer); + var border = new Border() + { + Name = PopupFramePart + }; + + TokenResourceBinder.CreateTokenBinding(popup, Popup.MarginToAnchorProperty, + NavMenuTokenResourceKey.TopLevelItemPopupMarginToAnchor); TokenResourceBinder.CreateTokenBinding(border, Border.CornerRadiusProperty, NavMenuTokenResourceKey.MenuPopupBorderRadius); TokenResourceBinder.CreateTokenBinding(border, Layoutable.MinWidthProperty, @@ -70,7 +75,7 @@ internal class NavMenuItemTheme : BaseNavMenuItemTheme NavMenuTokenResourceKey.TopLevelItemPopupMarginToAnchor); TokenResourceBinder.CreateTokenBinding(popup, Popup.MaskShadowsProperty, NavMenuTokenResourceKey.MenuPopupBoxShadows); - CreateTemplateParentBinding(popup, Avalonia.Controls.Primitives.Popup.IsOpenProperty, + CreateTemplateParentBinding(popup, Popup.IsOpenProperty, NavMenuItem.IsSubMenuOpenProperty, BindingMode.TwoWay); return popup; @@ -82,5 +87,19 @@ internal class NavMenuItemTheme : BaseNavMenuItemTheme var itemsPanelStyle = new Style(selector => selector.Nesting().Template().Name(ItemsPresenterPart).Child().OfType()); itemsPanelStyle.Add(StackPanel.SpacingProperty, NavMenuTokenResourceKey.VerticalItemsPanelSpacing); Add(itemsPanelStyle); + + { + var popupFrameStyle = new Style(selector => selector.Nesting().Template().Name(PopupFramePart)); + popupFrameStyle.Add(Border.BackgroundProperty, GlobalTokenResourceKey.ColorBgContainer); + Add(popupFrameStyle); + } + + var darkCommonStyle = new Style(selector => selector.Nesting().PropertyEquals(NavMenuItem.IsDarkStyleProperty, true)); + { + var popupFrameStyle = new Style(selector => selector.Nesting().Template().Name(PopupFramePart)); + popupFrameStyle.Add(Border.BackgroundProperty, NavMenuTokenResourceKey.DarkItemBg); + darkCommonStyle.Add(popupFrameStyle); + } + Add(darkCommonStyle); } } \ No newline at end of file diff --git a/src/AtomUI.Controls/NavMenu/NavMenuTheme.cs b/src/AtomUI.Controls/NavMenu/NavMenuTheme.cs index 7abb330..11d438f 100644 --- a/src/AtomUI.Controls/NavMenu/NavMenuTheme.cs +++ b/src/AtomUI.Controls/NavMenu/NavMenuTheme.cs @@ -72,7 +72,6 @@ internal class NavMenuTheme : BaseControlTheme var commonStyle = new Style(selector => selector.Nesting()); commonStyle.Add(TemplatedControl.BorderBrushProperty, GlobalTokenResourceKey.ColorBorderSecondary); - commonStyle.Add(TemplatedControl.CornerRadiusProperty, GlobalTokenResourceKey.BorderRadius); var horizontalStyle = new Style(selector => selector.Nesting().Class(NavMenu.HorizontalModePC)); horizontalStyle.Add(NavMenu.BackgroundProperty, GlobalTokenResourceKey.ColorBgContainer); @@ -92,6 +91,7 @@ internal class NavMenuTheme : BaseControlTheme var verticalOrInlineStyle = new Style(selector => Selectors.Or(selector.Nesting().Class(NavMenu.VerticalModePC), selector.Nesting().Class(NavMenu.InlineModePC))); + verticalOrInlineStyle.Add(NavMenu.PaddingProperty, NavMenuTokenResourceKey.VerticalMenuContentPadding); verticalOrInlineStyle.Add(NavMenu.HorizontalAlignmentProperty, HorizontalAlignment.Left); verticalOrInlineStyle.Add(NavMenu.VerticalAlignmentProperty, VerticalAlignment.Stretch); verticalOrInlineStyle.Add(NavMenu.BackgroundProperty, GlobalTokenResourceKey.ColorBgContainer); diff --git a/src/AtomUI.Controls/NavMenu/NavMenuToken.cs b/src/AtomUI.Controls/NavMenu/NavMenuToken.cs index 56f7c8e..a1757cd 100644 --- a/src/AtomUI.Controls/NavMenu/NavMenuToken.cs +++ b/src/AtomUI.Controls/NavMenu/NavMenuToken.cs @@ -190,6 +190,11 @@ internal class NavMenuToken : AbstractControlDesignToken /// public double VerticalItemsPanelSpacing { get; set; } + /// + /// 垂直面板的内容内间距 + /// + public Thickness VerticalMenuContentPadding { get; set; } + /// /// 菜单项内部元素边距 /// @@ -444,7 +449,7 @@ internal class NavMenuToken : AbstractControlDesignToken MenuPopupMinWidth = 160d; MenuPopupMaxWidth = 800d; MenuPopupMaxHeight = ItemHeight * 30; - TopLevelItemPopupMarginToAnchor = _globalToken.MarginXXS; + TopLevelItemPopupMarginToAnchor = _globalToken.MarginXS; MenuPopupBg = _globalToken.ColorBgElevated; MenuPopupBorderRadius = _globalToken.BorderRadiusLG; @@ -452,6 +457,7 @@ internal class NavMenuToken : AbstractControlDesignToken MenuPopupBoxShadows = _globalToken.BoxShadowsSecondary; VerticalItemsPanelSpacing = _globalToken.MarginXXS; - InlineItemIndentUnit = ItemHeight / 2; + InlineItemIndentUnit = ItemHeight / 2; + VerticalMenuContentPadding = new Thickness(_globalToken.PaddingXXS); } } \ No newline at end of file diff --git a/src/AtomUI.Controls/PathIcon/PathIcon.cs b/src/AtomUI.Controls/PathIcon/PathIcon.cs index cea5997..9b13531 100644 --- a/src/AtomUI.Controls/PathIcon/PathIcon.cs +++ b/src/AtomUI.Controls/PathIcon/PathIcon.cs @@ -471,16 +471,16 @@ public sealed class PathIcon : Control, ICustomHitTest base.OnAttachedToLogicalTree(e); SetupTransitions(); SetupRotateAnimation(); - if (_sourceGeometriesData.Count == 0) - { - BuildSourceRenderData(); - SetupFilledBrush(); - } } protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { base.OnAttachedToVisualTree(e); + if (_sourceGeometriesData.Count == 0) + { + BuildSourceRenderData(); + SetupFilledBrush(); + } if (_animation is not null && _animationCancellationTokenSource is null) { _animationCancellationTokenSource = new CancellationTokenSource(); @@ -516,28 +516,25 @@ public sealed class PathIcon : Control, ICustomHitTest protected override Size ArrangeOverride(Size finalSize) { - if (_sourceGeometriesData.Count != 0) + _transforms.Clear(); + // This should probably use GetRenderBounds(strokeThickness) but then the calculations + // will multiply the stroke thickness as well, which isn't correct. + for (var i = 0; i < _sourceGeometriesData.Count; i++) { - // This should probably use GetRenderBounds(strokeThickness) but then the calculations - // will multiply the stroke thickness as well, which isn't correct. - for (var i = 0; i < _sourceGeometriesData.Count; i++) - { - var sourceGeometry = _sourceGeometriesData[i]; - var (_, transform) = CalculateSizeAndTransform(finalSize, sourceGeometry.Bounds); - _transforms.Insert(i, transform); - } - - return finalSize; + var sourceGeometry = _sourceGeometriesData[i]; + var (_, transform) = CalculateSizeAndTransform(finalSize, sourceGeometry.Bounds); + _transforms.Insert(i, transform); } - return default; + return finalSize; } public override void Render(DrawingContext context) { - if (_sourceGeometriesData.Count > 0 && - DesiredSize.Width > 0 && - DesiredSize.Width > 0) + if (IsVisible && + _sourceGeometriesData.Count > 0 && + Bounds.Width > 0 && + Bounds.Width > 0) { for (var i = 0; i < _sourceGeometriesData.Count; i++) { @@ -577,7 +574,7 @@ public sealed class PathIcon : Control, ICustomHitTest { fillBrush = FilledBrush; } - + using var state = context.PushTransform(_transforms[i]); context.DrawGeometry(fillBrush, null, renderedGeometry); } diff --git a/src/AtomUI.Controls/Popup/Popup.cs b/src/AtomUI.Controls/Popup/Popup.cs index 84e7e76..888cc89 100644 --- a/src/AtomUI.Controls/Popup/Popup.cs +++ b/src/AtomUI.Controls/Popup/Popup.cs @@ -70,8 +70,8 @@ public class Popup : AvaloniaPopup private IDisposable? _selfLightDismissDisposable; private bool _initialized; private ManagedPopupPositionerInfo? _managedPopupPositioner; - protected bool _animating; private bool _isNeedFlip = true; + protected bool _animating; // 当鼠标移走了,但是打开动画还没完成,我们需要记录下来这个信号 internal bool RequestCloseWhereAnimationCompleted { get; set; } diff --git a/src/AtomUI.Controls/Switch/ToggleSwitch.cs b/src/AtomUI.Controls/Switch/ToggleSwitch.cs index 52d19ad..36dd059 100644 --- a/src/AtomUI.Controls/Switch/ToggleSwitch.cs +++ b/src/AtomUI.Controls/Switch/ToggleSwitch.cs @@ -309,7 +309,7 @@ public class ToggleSwitch : ToggleButton, _switchKnob.KnobSize = new Size(handleSize, handleSize); } - CalculateElementsOffset(DesiredSize); + CalculateElementsOffset(Bounds.Size); } protected override void OnPointerPressed(PointerPressedEventArgs e) @@ -524,7 +524,7 @@ public class ToggleSwitch : ToggleButton, CollectStyleState(); if (e.Property == IsCheckedProperty) { - CalculateElementsOffset(DesiredSize); + CalculateElementsOffset(Bounds.Size); WaveSpiritAdorner.ShowWaveAdorner(this, WaveType.PillWave); } } @@ -546,12 +546,12 @@ public class ToggleSwitch : ToggleButton, private Rect GrooveRect() { - return new Rect(new Point(0, 0), DesiredSize); + return new Rect(new Point(0, 0), Bounds.Size); } private Rect HandleRect() { - return HandleRect(IsChecked.HasValue && IsChecked.Value, DesiredSize); + return HandleRect(IsChecked.HasValue && IsChecked.Value, Bounds.Size); } private Rect HandleRect(bool isChecked, Size controlSize)